Objective-C 不是你想的那樣
(via:morisunshine blog)原文:Objective-C isn't what you think it is"
Ruby 和 Objective-C 這兩種語言看上去好像天南地北:一種是動態(tài)語言,另一種則是靜態(tài)語言;一種是解釋型語言,另一種是編譯型語言;一種有簡潔的語法,另一種則是有點冗長的語 法。從優(yōu)雅的角度來看,Ruby似乎更能給我們一種自由的編程體驗,所以很多人都放棄了Objective-C。
但這是一個不幸的笑話。Objective-C其實并不像別人認為的那樣是件緊身衣,它和Ruby一樣都受Smalltalk影響,它擁有很多 Ruby開發(fā)者都喜愛的語言功能–動態(tài)方法查找、鴨子類型、開放的類和通常情況下高度可變的runtime等這些功能在Objective-C中同樣存 在,即使那些不出名的技術也是一樣。Objective-C的這些功能都要歸功于它的IDE和編譯器,但也是因為它們才使你不能自由地編寫代碼。
但是等一下,怎么能說Objective-C是動態(tài)語言呢?難道它不是建立在C語言的基礎上?
你可以在Objective-C代碼中包含任何C或C++的代碼,但這不意味著Objective-C僅限于C或C++代碼。Objective- C中所有有意思的類操作和對象內(nèi)省都是來自于一個叫Objective-C Runtime的東西。這個Objective-C Runtime可以和Ruby解釋器相媲美。它包含了強大的元編程里所需要的所有重要特性。
其實C語言和Ruby一樣是支持這些特性的,用 property_getAttributes 或 method_getImplementation 方法就能將selector對應到具體實現(xiàn)(一個selector處理一個方法),并判斷這個對象能否對這個selector做出反應,再遍歷子類樹。在Objective-C的眾多方法中,最重要的就是objc_msgSend方法,是它推動了應用中的每次消息發(fā)送。
消息的傳遞
Smalltalk才是實至名歸的第一種面向?qū)ο笳Z言,它用“從一個對象發(fā)送信息給另一個對象”的新概念取代了“調(diào)用函數(shù)”的舊概念,對后面的語言發(fā)展產(chǎn)生了深遠的影響。
你可以在Ruby中通過這樣寫來實現(xiàn)消息的發(fā)送:
- receiver.the_message argument
Objective-C的實現(xiàn)方式和Ruby的差不多:
- [receiver theMessage:argument];
這些消息實現(xiàn)了鴨子類型的方式,也就是說關注的不是這個對象的類型或類本身,而是這個對象能否對一個消息做出反應。
發(fā)送消息真的是非常棒的事,但是只有當消息在傳送數(shù)據(jù)時,它的價值才會被發(fā)揮地更大:
- receiver.send(:the_message, argument)
和
- [receiver performSelector:@selector(theMessage:)
- withObject:argument];
正如Ruby中方法需要symbol支持一樣,Objective-C中selector也需要string來支持。(在Objective-C中沒有symbol。)這樣就可以讓你通過動態(tài)的方式使用一個方法。你甚至可以通過NSSelectorFromString 方法來使用string創(chuàng)建一個selector,并在一個對象里執(zhí)行它。同樣的,我們可以在Ruby中也可以創(chuàng)建一個string或symbol,并把傳給 Object#send 方法。
當然,無論是哪種語言,一旦你將一個消息發(fā)送給不能處理該消息的對象,那么默認情況下就會拋出一個異常,還會導致應用的崩潰。
當你想在調(diào)用一個方法前判斷一下這個對象是否能夠執(zhí)行這個方法,你可以用Ruby中的 respond_to?方法來檢查:
- if receiver.respond_to? :the_message
- receiver.the_message argument
- end
Objective-C中也有差不多的方法:
- if ([receiver respondsToSelector:@selector(theMessage:)]) {
- [receiver theMessage:someThing];
- }
變得越來越動態(tài)
如果你想在一個不能修改的類(像系統(tǒng)類)中添加你想要的方法,那么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;
- }
- @end
Category是在編譯的時候?qū)⒎椒ㄌ砑拥匠绦蛑?-- 讓我們在runtime中動態(tài)捕捉它們怎么樣?
有些消息可以嵌套數(shù)據(jù),就像Rails的dynamic finders。Ruby通過對 method_missing 和 respond_to 這兩個方法的重寫,先匹配模式,再將新方法的定義添加到這個對象中。
Objective-C中的流程是差不多,但我們不是重寫 doesNotRecognizeSelector:方法(相當于Ruby中的 method_missing方法),而是在 resolveClassMethod: 方法中捕捉Category添加的方法。假設我們有一個叫 +findWhere:equals: 的類方法,它可以得到property的名稱和值,那么通過正則表達式就可以很容易實現(xiàn)找到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];
- }
這個方法的優(yōu)點就是我們不需要去重寫 respondsToSelector:,因為每個在類中注冊過的selector都會去調(diào)用這個方法?,F(xiàn)在讓我們調(diào)用 [RGSong findWhereTitleEquals:@“Mercy”]。當 findWhereTitleEquals: 第一次被調(diào)用的時候,runtime 并不知道這個方法,所以它會調(diào)用 resolveClassMethod:,這時我們就將 findWhereTitleEquals:這個方法動態(tài)添加進去,當?shù)诙握{(diào)用findWhereTitleEquals:的時候,因為它已經(jīng)被添加過了,所以就不會再調(diào)用 resolveClassMethod:了。
這里還有一些別的方法來實現(xiàn)捕捉動態(tài)方法。你可以通過重寫 resolveClassMethod: 和 resolveInstanceMethod: 方法(就像上面的一樣),可以將消息傳遞給不同的對象或全權(quán)接管這個“調(diào)用”,并在消息傳遞之前,做你想這個消息要完成的任何事。這些方法都會導致運行成 本的增加,特別在-forwardInvocation:中會達到頂峰,在這種情況下我們必須要實例化一個對象才能去執(zhí)行它們。-forwardInvocation:方法中默認調(diào)用 doesNotRecognizeSelector 方法,這導致了應用的頻繁異?;虮罎?。
內(nèi)省
動態(tài)方法決議并不只是像Ruby和Objective-C這樣的語言的技術支持。你也可以通過在runtime中用一種有意思的方式去操作這些對象。
就像在Ruby中調(diào)用 MyClass#instance_methods 一樣,你可以在Objective-C中調(diào)用 class_copyMethodList([MyClass class], &numberOfMethods) 來得到一個對象中方法的列表。你還可以通過class_copyPropertyList 方法得到一個類中property的列表,它能在你的模型中實現(xiàn)不可思議的內(nèi)省。比如在這個Rap Genius應用中,我們用這個功能來將JSON中的字典映射到本地對象上。
(如果你非常喜歡Ruby中的mixin,那么Objective-C強大的動態(tài)支持也能能實現(xiàn)同樣的效果。 Vladimir Mitrovic有一個叫 Objective-Mixin 的庫,它能在runtime時將一個類中的實現(xiàn)復制到另一個類中。)
現(xiàn)學現(xiàn)用
所有的動態(tài)工具都可以用來創(chuàng)建像Core Data這樣的東西,Core Data是一個有點像ActiveRecord的持久化對象圖。在Core Data中,relationship是“有缺陷的”,也就是說他們只有在被別的對象訪問時,才會被加載。每個property的accessor和 mutator在runtime中都被重寫(使用的就是我們上面提到的動態(tài)方法決議)。如果我們訪問了一個還沒有被加載的對象時,框架就會從持久性儲存中 動態(tài)加載這個對象并將它返回。它保持了內(nèi)存的低利用率,避免了在任何一個物體被獲取時,實體對象圖表都要被加載到內(nèi)存中這樣情況的發(fā)生。
當Core Data實體中的mutator被調(diào)用時,系統(tǒng)會將那個對象標記為需要清理,不需要去重寫每個property的getter和setter。
這就是元程序,羨慕吧!
什么是編譯器?
很明顯,Objective-C和Ruby并不是同一種語言,目前為止最大的不同就是Objective-C是一種編譯型語言。
這就是這些技術中最需要注意的地方。在編譯時,編譯器會先確定你應用使用的每個selector是不是都在應用中。如果你處理的這個對象有類型信 息,那么編譯器也會檢查確保這個selector在頭文件有聲明過,這樣做就是為了防止在對象中調(diào)用未聲明的selector。有些方法可以繞過這些討厭 的限制,包括關閉相關的編譯警告。這里就是實踐元程序化的Objective-C最好的練習。
你可以通過將 selector 的類型儲存為不知道的類型或 id 來從對象中刪除這些類型信息。因為編譯器不認識這個類型,所以它只能假設你的程序可以接受發(fā)給它的任何消息(假設這些消息在應用中的其他地方被聲明了,并且相關的編譯標識已經(jīng)打開)。
善意的忠告:如果我們關掉編譯器標識和把對象保存成 id 類型,那么將會非常危險的事!其實Objective-C中最好的東西之一就是編譯器(是的,比元程序還要好)。類型檢查保證了我們更快的寫和重構(gòu)代碼,也是我們在編程時少犯錯誤。因為沒有人會關掉那些警告,所以你很難去分享你那些 id 類型的代碼。大部分Objective-C開發(fā)者還是更愿意使用更強的類型而不是元程序。
事實證明Objective-C更受束縛--但因為編譯器能提高更多的安全性和速度,所以我們只能選擇這樣并承擔后果。
事實再次告訴我們,這些語言都是差不多的,Ruby開發(fā)者應該享受Objective-C,即使那些中括號讓我們望而卻步。