天天看點

Objective-C 不是你想的那樣

Ruby 和 Objective-C 這兩種語言看上去好像天南地北:一種是動态語言,另一種則是靜态語言;一種是解釋型語言,另一種是編譯型語言;一種有簡潔的文法,另一種則是有點冗長的文法。從優雅的角度來看,Ruby似乎更能給我們一種自由的程式設計體驗,是以很多人都放棄了Objective-C。

但這是一個不幸的笑話。Objective-C其實并不像别人認為的那樣是件緊身衣,它和Ruby一樣都受Smalltalk影響,它擁有很多Ruby開發者都喜愛的語言功能–動态方法查找、鴨子類型、開放的類和通常情況下高度可變的runtime等這些功能在Objective-C中同樣存在,即使那些不出名的技術也是一樣。Objective-C的這些功能都要歸功于它的IDE和編譯器,但也是因為它們才使你不能自由地編寫代碼

但是等一下,怎麼能說Objective-C是動态語言呢?難道它不是建立在C語言的基礎上?

你可以在Objective-C代碼中包含任何C或C++的代碼,但這不意味着Objective-C僅限于C或C++代碼。Objective-C中所有有意思的類操作和對象内省都是來自于一個叫Objective-C Runtime的東西。這個Objective-C Runtime可以和Ruby解釋器相媲美。它包含了強大的元程式設計裡所需要的所有重要特性。

其實C語言和Ruby一樣是支援這些特性的,用

property_getAttributes

method_getImplementation

方法就能将selector對應到具體實作(一個selector處理一個方法),并判斷這個對象能否對這個selector做出反應,再周遊子類樹。在Objective-C的衆多方法中,最重要的就是

objc_msgSend

方法,是它推動了應用中的每次消息發送。
Objective-C 不是你想的那樣

消息的傳遞

Smalltalk才是實至名歸的第一種面向對象語言,它用“從一個對象發送資訊給另一個對象”的新概念取代了“調用函數”的舊概念,對後面的語言發展産生了深遠的影響。

你可以在Ruby中通過這樣寫來實作消息的發送:

receiver.the_message argument

Objective-C的實作方式和Ruby的差不多:

[receiver theMessage:argument];

這些消息實作了鴨子類型的方式,也就是說關注的不是這個對象的類型或類本身,而是這個對象能否對一個消息做出反應。

發送消息真的是非常棒的事,但是隻有當消息在傳送資料時,它的價值才會被發揮地更大:

receiver.send(:the_message, argument)

[receiver performSelector:@selector(theMessage:) 

withObject:argument];

正如Ruby中方法需要symbol支援一樣,Objective-C中selector也需要string來支援。(在Objective-C中沒有symbol。)這樣就可以讓你通過動态的方式使用一個方法。你甚至可以通過

NSSelectorFromString

方法來使用string建立一個selector,并在一個對象裡執行它。同樣的,我們可以在Ruby中也可以建立一個string或symbol,并把傳給

Object#send

方法。

當然,無論是哪種語言,一旦你将一個消息發送給不能處理該消息的對象,那麼預設情況下就會抛出一個異常,還會導緻應用的崩潰。

當你想在調用一個方法前判斷一下這個對象是否能夠執行這個方法,你可以用Ruby中的

respond_to?

方法來檢查:

if receiver.respond_to? :the_message

  receiver.the_message argument

end

Objective-C中也有差不多的方法:

if ([receiver respondsToSelector:@selector(theMessage:)]) {

    [receiver theMessage:someThing];

}

變得越來越動态

如果你想在一個不能修改的類(像系統類)中添加你想要的方法,那麼Objective-C裡的category一定不會讓你失望 — 很像Ruby中的“開放類”。

舉個例子,如果你想将Rails中的

to_sentence

方法添加到

NSArray

類中,我們隻需要對

NSArray

這個類進行擴充就好了:

@interface NSArray (ToSentence)

- (NSString *)toSentence;

@end

@implementation NSArray (ToSentence)

- (NSString *)toSentence {

    if (self.count == 0) return @"";

    if (self.count == 1) return [self lastObject];

    NSArray *allButLastObject = [self subarrayWithRange:NSMakeRange(0, self.count-1)];

    NSString *result = [allButLastObject componentsJoinedByString:@", "];

    BOOL showComma = self.count > 2;

    result = [result stringByAppendingFormat:@"%@ and ", showComma ? @"," : @""];

    result = [result stringByAppendingString:[self lastObject]];

    return result;

Category是在編譯的時候将方法添加到程式中 — 讓我們在runtime中動态捕捉它們怎麼樣?

有些消息可以嵌套資料,就像Rails的dynamic finders。Ruby通過對

method_missing

respond_to

這兩個方法的重寫,先比對模式,再将新方法的定義添加到這個對象中。

Objective-C中的流程是差不多,但我們不是重寫

doesNotRecognizeSelector:

方法(相當于Ruby中的

method_missing

方法),而是在

resolveClassMethod:

方法中捕捉Category添加的方法。假設我們有一個叫

+findWhere:equals:

的類方法,它可以得到property的名稱和值,那麼通過正規表達式就可以很容易實作找到property的名字,并通過block來注冊這個selector。

+ (BOOL)resolveClassMethod:(SEL)sel {

    NSString *selectorName = NSStringFromSelector(sel);

    NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^findWhere(\\w+)Equals:$" options:0 error:nil];

    NSTextCheckingResult *result = [regex firstMatchInString:selectorName options:0 range:NSMakeRange(0, selectorName.length)];

    if (result) {

        NSRange propertyNameRange = [result rangeAtIndex:1];

        NSString *propertyName = [selectorName substringWithRange:propertyNameRange];

        IMP implementation  = imp_implementationWithBlock((id) ^(id self, id arg1) {

            return [self findWhere:propertyName equals:arg1];

        });

        Class metaClass = object_getClass(self);

        class_addMethod(metaClass, sel, implementation, "@@:@@");

        return YES;

    }

    return [super resolveClassMethod:sel];

這個方法的優點就是我們不需要去重寫

respondsToSelector:

,因為每個在類中注冊過的selector都會去調用這個方法。現在讓我們調用

[RGSong findWhereTitleEquals:@“Mercy”]

。當

findWhereTitleEquals:

第一次被調用的時候,runtime并不知道這個方法,是以它會調用

resolveClassMethod:

,這時我們就将

findWhereTitleEquals:

這個方法動态添加進去,當第二次調用

findWhereTitleEquals:

的時候,因為它已經被添加過了,是以就不會再調用

resolveClassMethod:

了。

這裡還有一些别的方法來實作捕捉動态方法。你可以通過重寫

resolveClassMethod:

resolveInstanceMethod:

方法(就像上面的一樣),可以将消息傳遞給不同的對象或全權接管這個“調用”,并在消息傳遞之前,做你想這個消息要完成的任何事。這些方法都會導緻運作成本的增加,特别在

-forwardInvocation:

中會達到頂峰,在這種情況下我們必須要執行個體化一個對象才能去執行它們。

-forwardInvocation:

方法中預設調用

doesNotRecognizeSelector

方法,這導緻了應用的頻繁異常或崩潰。

内省

動态方法決議并不隻是像Ruby和Objective-C這樣的語言的技術支援。你也可以通過在runtime中用一種有意思的方式去操作這些對象。

就像在Ruby中調用

MyClass#instance_methods

一樣,你可以在Objective-C中調用

class_copyMethodList([MyClass class], &numberOfMethods)

來得到一個對象中方法的清單。你還可以通過

class_copyPropertyList

方法得到一個類中property的清單,它能在你的模型中實作不可思議的内省。比如在這個

Rap Genius

應用中,我們用這個功能來将JSON中的字典映射到本地對象上。

(如果你非常喜歡Ruby中的mixin,那麼Objective-C強大的動态支援也能能實作同樣的效果。 Vladimir Mitrovic有一個叫

Objective-Mixin

的庫,它能在runtime時将一個類中的實作複制到另一個類中。)

現學現用

所有的動态工具都可以用來建立像Core Data這樣的東西,Core Data是一個有點像ActiveRecord的持久化對象圖。在Core Data中,relationship是“有缺陷的”,也就是說他們隻有在被别的對象通路時,才會被加載。每個property的accessor和mutator在runtime中都被重寫(使用的就是我們上面提到的動态方法決議)。如果我們通路了一個還沒有被加載的對象時,架構就會從持久性儲存中動态加載這個對象并将它傳回。它保持了記憶體的低使用率,避免了在任何一個物體被擷取時,實體對象圖表都要被加載到記憶體中這樣情況的發生。

當Core Data實體中的mutator被調用時,系統會将那個對象标記為需要清理,不需要去重寫每個property的getter和setter。

這就是元程式,羨慕吧!

什麼是編譯器?

很明顯,Objective-C和Ruby并不是同一種語言,目前為止最大的不同就是Objective-C是一種編譯型語言。

這就是這些技術中最需要注意的地方。在編譯時,編譯器會先确定你應用使用的每個selector是不是都在應用中。如果你處理的這個對象有類型資訊,那麼編譯器也會檢查確定這個selector在頭檔案有聲明過,這樣做就是為了防止在對象中調用未聲明的selector。有些方法可以繞過這些讨厭的限制,包括關閉相關的編譯警告。這裡就是實踐元程式化的Objective-C最好的練習。

你可以通過将selector的類型儲存為不知道的類型或

id

來從對象中删除這些類型資訊。因為編譯器不認識這個類型,是以它隻能假設你的程式可以接受發給它的任何消息(假設這些消息在應用中的其他地方被聲明了,并且相關的編譯辨別已經打開)。

善意的忠告:如果我們關掉編譯器辨別和把對象儲存成

id

類型,那麼将會非常危險的事!其實Objective-C中最好的東西之一就是編譯器(是的,比元程式還要好)。類型檢查保證了我們更快的寫和

重構

代碼,也是我們在程式設計時少犯錯誤。因為沒有人會關掉那些警告,是以你很難去分享你那些

id

類型的代碼。大部分Objective-C開發者還是更願意使用更強的類型而不是元程式。

事實證明Objective-C更受束縛–但因為編譯器能提高更多的安全性和速度,是以我們隻能選擇這樣并承擔後果。

事實再次告訴我們,這些語言都是差不多的,Ruby開發者應該享受Objective-C,即使那些中括号讓我們望而卻步。

繼續閱讀