Objective-C 不是你想的那樣

墨日陽光發表於2014-03-27

 訊息的傳遞
 變得越來越動態
 內省
 現學現用
 什麼是編譯器?

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_getAttributesmethod_getImplementation方法就能將selector對應到具體實現(一個selector處理一個方法),並判斷這個物件能否對這個selector做出反應,再遍歷子類樹。在Objective-C的眾多方法中,最重要的就是objc_msgSend方法,是它推動了應用中的每次訊息傳送。


訊息的傳遞

Smalltalk才是實至名歸的第一種面嚮物件語言,它用“從一個物件傳送資訊給另一個物件”的新概念取代了“呼叫函式”的舊概念,對後面的語言發展產生了深遠的影響。

你可以在Ruby中通過這樣寫來實現訊息的傳送:

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

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

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

正如Ruby中方法需要symbol支援一樣,Objective-C中selector也需要string來支援。(在Objective-C中沒有symbol。)這樣就可以讓你通過動態的方式使用一個方法。你甚至可以通過NSSelectorFromString方法來使用string建立一個selector,並在一個物件裡執行它。同樣的,我們可以在Ruby中也可以建立一個string或symbol,並把傳給Object#send方法。

當然,無論是哪種語言,一旦你將一個訊息傳送給不能處理該訊息的物件,那麼預設情況下就會丟擲一個異常,還會導致應用的崩潰。

當你想在呼叫一個方法前判斷一下這個物件是否能夠執行這個方法,你可以用Ruby中的respond_to?方法來檢查:

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


變得越來越動態

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

舉個例子,如果你想將Rails中的to_sentence方法新增到NSArray類中,我們只需要對NSArray這個類進行擴充套件就好了:

Category是在編譯的時候將方法新增到程式中 — 讓我們在runtime中動態捕捉它們怎麼樣?

有些訊息可以巢狀資料,就像Rails的dynamic finders。Ruby通過對method_missingrespond_to這兩個方法的重寫,先匹配模式,再將新方法的定義新增到這個物件中。

Objective-C中的流程是差不多,但我們不是重寫doesNotRecognizeSelector:方法(相當於Ruby中的method_missing方法),而是在resolveClassMethod:方法中捕捉Category新增的方法。假設我們有一個叫+findWhere:equals:的類方法,它可以得到property的名稱和值,那麼通過正規表示式就可以很容易實現找到property的名字,並通過block來註冊這個selector。

這個方法的優點就是我們不需要去重寫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,即使那些中括號讓我們望而卻步。

相關文章