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
方法,是它推動了應用中的每次訊息傳送。
訊息的傳遞
Smalltalk才是實至名歸的第一種面嚮物件語言,它用“從一個物件傳送資訊給另一個物件”的新概念取代了“呼叫函式”的舊概念,對後面的語言發展產生了深遠的影響。
你可以在Ruby中通過這樣寫來實現訊息的傳送:
1 2 3 |
; html-script: false ] receiver.the_message argument |
Objective-C的實現方式和Ruby的差不多:
1 2 3 |
; html-script: false ] [receiver theMessage:argument]; |
這些訊息實現了鴨子型別的方式,也就是說關注的不是這個物件的型別或類本身,而是這個物件能否對一個訊息做出反應。
傳送訊息真的是非常棒的事,但是隻有當訊息在傳送資料時,它的價值才會被髮揮地更大:
1 2 3 |
; html-script: false ] receiver.send(:the_message, argument) |
和
1 2 3 4 |
; html-script: false ] [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?
方法來檢查:
1 2 3 4 5 |
; html-script: false ] if receiver.respond_to? :the_message receiver.the_message argument end |
Objective-C中也有差不多的方法:
1 2 3 4 5 |
; html-script: false ] if ([receiver respondsToSelector:@selector(theMessage:)]) { [receiver theMessage:someThing]; } |
變得越來越動態
如果你想在一個不能修改的類(像系統類)中新增你想要的方法,那麼Objective-C裡的category一定不會讓你失望 — 很像Ruby中的“開放類”。
舉個例子,如果你想將Rails中的to_sentence
方法新增到NSArray
類中,我們只需要對NSArray
這個類進行擴充套件就好了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
; html-script: false ] @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是在編譯的時候將方法新增到程式中 — 讓我們在runtime中動態捕捉它們怎麼樣?
有些訊息可以巢狀資料,就像Rails的dynamic finders。Ruby通過對method_missing
和 respond_to
這兩個方法的重寫,先匹配模式,再將新方法的定義新增到這個物件中。
Objective-C中的流程是差不多,但我們不是重寫doesNotRecognizeSelector:
方法(相當於Ruby中的method_missing
方法),而是在resolveClassMethod:
方法中捕捉Category新增的方法。假設我們有一個叫+findWhere:equals:
的類方法,它可以得到property的名稱和值,那麼通過正規表示式就可以很容易實現找到property的名字,並通過block來註冊這個selector。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
; html-script: false ] + (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,即使那些中括號讓我們望而卻步。