Objective-C Class/Object 到底是什麼?

你應該在其他的文件裡頭聽說過,Objective-C 是 C 語言的 Superset,在 C語言的基礎上,加上了一層稀薄的物件導向,而 Cocoa Framework 的 Cocoa這個名字就是這麼來的—Cocoa 就是 C 加上 OO。也因此,在 Objective-C程式中,可以直接呼叫 C 的 API,而如果你將 .m 改名叫做.mm,程式裡頭還可以混和 C++ 語法,變成 Objective-C++。

Objective-C 的程式在 compile time 時,Compiler 其實會編譯成 C然後繼續編譯。所有的Objective-C Class 會變成 C 的 Structure,所有的method (以及 block)會被編譯成 C function,接下來,在執行的時候,Objective-C runtime 才會建立某個 C Structure 與 C function 的關聯,也就是說,一個物件到底有哪些 method可以呼叫,是在 runtime 才決定的。

Objective-C 物件會被編譯成 Structure

比方說,我們現在寫了一個簡單的 Class,裡頭只有 int a 這個成員變數:

@interface MyClass : NSObject {
    int a;
}
@end

會被編譯成

typedef struct {
    int a;
} MyClass;

因為 Objective-C 的物件其實就是 C 的 structure,所以當我們建立了一個 Objective-C 物件之後,我們也可以把這個物件當做呼叫 C structure 呼叫 1

MyClass *obj = [[MyClass alloc] init];
obj->a = 10;

對 Class 加入 method

在執行的時候,runtime 會為每個 class 準備好一張表格(專用術語叫做 virtual table),表格裡頭會以一個字串當 key,每個 key 會對應到 C function 的指標位置。Run time 裡頭,把實作的 C function 定義成 IMP 這個type;至於拿來當作 key 的字串,就叫做 selector,type 定義成 SEL,然後我們可以使用 @selector 關鍵字建立 selector。

selector1.png

而其實 SEL 就是 C 字串,我們可以來寫點程式檢查一下:

NSLog(@"%s", (char *)(@selector(doSomething)));

我們會順利印出「doSomething」這個 C 字串。

xcode

每次我們對一個物件呼叫某個 method,runtime 在做的事情,就是把 method的名稱當做字串,尋找與字串符合的 C function實作,然後執行。也就是說,下面這三件事情是一樣的:

我們可以直接要求某個物件執行某個 method:

[myObject doSomthing];

或是透過 performSelector: 呼叫。 performSelector:NSObject 的 method,而在 Cocoa Framework 中所有的物件都繼承自 NSObject,所以每個物件都可以呼叫這個 method。

[myObject performSelector:@selector(doSomething)];

我們可以把 performSelector:想成台灣的電視新聞用語:如果原本的句子是「我正在吃飯」,使用performSelector:就很像是「我正在進行一個吃飯的動作」。而其實,最後底層執行的是objc_msgSend

objc_msgSend(myObject, @selector(doSomething), NULL);

我們常常會說「要求某個 object 執行某個 methood」、「要求某個 object執行某個 selector」,其實是一樣的事情,我們另外也常聽到一種說法,叫做「對 receiver 傳遞message」,這則是沿用來自 Small Talk 的術語—Objective-C 受到了 Small Talk 語言的深刻影響—但其實也是同一件事。

因為一個 Class 有哪些 method,是在 run time 一個一個加入的;所以我們就有機會在程式已經在執行的時候,繼續對某個 Class 加入新 method,一個 Class已經存在了某個 method,也可以在 run time 用別的實作換掉,一般來說,我們會用 Category 做這件事情,不過 Category會是下一章的主題,會在下一章繼續討論。

我們在這裡首先要記住一件非常重要的事:在 Objective-C 中,一個 class 會有哪些 method,並不是固定的,如果我們在程式中對某個物件呼叫了目前還不存在的 method,編譯的時候,compiler 並不會當做編譯錯誤,只會發出警告而已,而跳出警告的條件,也就只有是否有引入的 header 中到底有沒有這個 method而已,所以我們一不小心,就很有可能呼叫到了沒有實作的method(或這麼說,我們要求執行的 selector並沒有對應的實作)。如果我們是使用 performSelector:呼叫,更是完全不會有警告。直到實際執行的時候,才發生 unrecognized selector sent to instance 錯誤而導致應用程式 crash。

之所以只有警告,而不當做編譯錯誤,就是因為某些 method有可能之後才會被加入。蘋果認為你會寫出呼叫到沒有實作的selector,必定是因為你接下來在某個時候、某個地方,就會加入這個 method的實作。

由於 Objective-C 語言中,物件有哪些 method 可以在 run time 改變,所以我們也會將 Objective-C 列入像是 Perl、Python、Ruby等所謂的動態語言(Dynamic Language)之林。而在寫這樣的動態物件導向語言時,一個物件到底有哪些method 可以呼叫,往往會比這個物件到底是屬於哪個 class 更為重要。 2

如果我們不想要用 category,而想要自己動手寫點程式,手動將某些 method 加入到某個 class 中,我們可以這麼寫。首先宣告一個 C function,至少要有兩個參數,第一個參數是執行 method 的物件,第二個參數是 selector,像這樣:

void myMethodIMP(id self, SEL _cmd) {
    doSomething();
}

接下來可以呼叫 class_addMethod 加入 selector 與實作的對應。

#import <objc/runtime.h>
// 中間省略
class_addMethod([MyClass class], @selector(myMethod), (IMP)myMethodIMP, "v@:");

接下來就可以這麼呼叫了:

MyClass *myObject = [[MyClass alloc] init];
[myObject myMethod];

selector2.png

1. 不過,如果你直接在程式裡頭這麼呼叫,Xcode 會在編譯的時候發出警告,告訴你在不久的將來會禁止這樣直接呼叫物件的成員變數,如果想要取用成員變數,必須另外寫 getter/setter。而如果這個成員變數被宣告成是 private 的,Xcode 會直接出現編譯錯誤,禁止你這樣呼叫。
2. 這種強調物件有哪些 method,會比物件繼承自哪個 Class 來得重要的觀念,有一個專有名詞,叫做 Duck Typing,中文翻譯做「鴨子型別」。觀念是:我眼前這個東西到底是不是鴨子?它是不是鳥類或是哪個種類根本就不重要,反正它走路游泳像鴨子,叫起來像鴨子,那我就當它是鴨子。可以參見Wikipedia 上的說明: http://en.wikipedia.org/wiki/Duck_typing

results matching ""

    No results matching ""