天天看點

《Effective Ruby:改善Ruby程式的48條建議》一第6條:了解Ruby如何建構繼承體系

本節書摘來自華章出版社《effective ruby:改善ruby程式的48條建議》一書中的第2章,第2.1節,作者 [美]彼得 j.瓊斯(peter j. jones),更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視

問題:當你向一個對象發送消息時,你知道ruby是怎麼定位到那個正确的方法的嗎?答案看上去很簡單:使用繼承體系。之是以說答案是看上去簡單,因為ruby建構繼承體系是在幕後進行的。這是ruby用自己的方式來隐藏真正發生的事情的罕見情形之一,它常常帶來不必要的困惑。ruby提供給我們的用來發現繼承關系中的類的方法并能完全解釋其本質。還好,ruby解釋器在内部建構的繼承體系是一緻而簡單的,隻要你明白一些它的技巧。

探索ruby查詢方法的機制将揭開語言實作方式的面紗,并為我們提供解開類的真正繼承體系的完美環境。這也是你應該知道的事情。好消息是讀完這一條,你不會再對ruby的對象模型感到驚訝。壞消息是我們需要從複習經典的oop術語及一些常見的術語定義開始。下面這幾個段落,你得忍忍。

對象是變量的容器。這些變量被稱為執行個體變量并代表了一個對象的狀态。每個對象都有一個特殊的内部變量來連接配接唯一的一個類。由于這個連接配接的存在,我們稱該對象是該類的一個執行個體。

類是方法和常量的容器。這些方法被稱為執行個體方法并代表了類的所有執行個體對象的行為。

事情變得有點困惑和迂回,因為類本身也是對象。是以,每個類也是一個被稱為類變量的容器。這些類對象,像所有其他對象一樣,也是有方法的。從技術上講,這些方法與執行個體方法沒有差別,但為了避免過度混淆,是以被稱為類方法。換句話說,類是一種其變量被稱為類變量、其方法被稱為類方法的對象。(類對象也可以有執行個體變量,但我們會在第15條再讨論它)。

超類(superclass)是在類層次結構中表示一個類的父類的花哨的名字。如果類b繼承自類a,那麼類a是類b的超類。類有其特殊的内部變量來跟蹤其超類。

子產品和類除了一點以外在各方面都是相同的。是以,像類一樣,子產品也是對象,并且是一個特定類的執行個體。類是類class的執行個體,而子產品是類module的執行個體。ruby在内部實作子產品和類使用了相同的資料結構,但限制了通過它們的類方法能做的事情(子產品是沒有new方法的)以及更嚴格的文法。

子產品在ruby中有很多用法,但現在我們隻關心它在繼承體系中的作用。盡管ruby并不直接支援多繼承,但可以通過include方法将子產品引入類,這實作了和多繼承類似的效果。

單例類(singleton class)是個令人困惑的名詞,因為它是繼承體系中一個匿名且不可見的類。

這個類從目的上帶來的困惑并不比從名字上帶來得多。有時候它們被稱為本征類(eigenclasses)或是元類(metaclasses)。甚至ruby解釋器的源碼中也是交換地使用着這些術語。本書中,我将始終稱其為單例類,因為在ruby中當你使用内省方法(introspection methods)如singleton_class和singleton_methods時它的名字就叫作單例。

單例類在ruby中承擔了重要角色,比如提供了存儲類方法和從modules中引入的方法的空間。不像其他的類,它們是ruby自身根據需要動态建立的。它們的建立也是有限制的。比如,你不能建立一個單例類的執行個體。你唯一需要記住的是,單例類僅僅是沒有名字的、被加以限制的正常類。

接收者是調用方法的那個對象。比如,在“customer.name”中,被調用的方法是name而接收者是customer。當name方法執行時,self變量被設定在customer上,任何執行個體變量所通路的都将是customer對象。有時候接收者在方法調用時被省略,這時會隐式地将self設定為目前上下文。

哇!現在我們終于完成了ruby對象模型中的詞彙課程以及它們建構繼承體系的方式。讓我們開始裝配一個小的類體系并在irb中運作它。

《Effective Ruby:改善Ruby程式的48條建議》一第6條:了解Ruby如何建構繼承體系

這段代碼沒有什麼讓人驚訝的。如果你調用customer對象的name方法,方法的查找會和你預期的完全一樣。首先,customer類會檢查自身是否有這個執行個體方法。顯然沒有,于是繼續搜尋,順着繼承體系向上找到了person類,在該類中找到了該方法并将其執行。如果在person類中沒有找到的話,ruby會繼續向上查找直到找到該方法或者到達根類basicobject。你可以從!

《Effective Ruby:改善Ruby程式的48條建議》一第6條:了解Ruby如何建構繼承體系

如你所知,如果方法在查找過程中直到類樹的根節點仍然沒有找到比對的方法,那麼它将重新從起點開始查找,不過這一次會查找method_missing方法。第30條會請你自行定義method_missing方法,是以這裡我們不再涉及。也就是說,除了kernel子產品中method_missing的預設實作會引發異常告訴你說你調用了一個未定義的方法以外,不會發生别的什麼了。

這提醒了我:是時候舉個簡單的例子來制造點困難了:

《Effective Ruby:改善Ruby程式的48條建議》一第6條:了解Ruby如何建構繼承體系

我已經将name方法從person類中取出并移到一個子產品中,并将子產品引入了person類。customer類的執行個體仍然可以如你所料響應name方法,但是為什麼呢?顯然,子產品thingswithnames并不在繼承體系中,因為person類的超類仍然是object類,那會是什麼呢?其實,ruby在這裡對你撒謊了,我們需要多講一點單例類。

當你使用include方法來将子產品引入類時,ruby在幕後悄悄地做了些事情。它建立了一個單例類并将它插入類體系中。這個匿名的不可見類被鍊向這個子產品,是以它們共享了執行個體方法和常量。本例中person類在包含子產品thingswithnames時,ruby建立了一個單例類并悄悄地作為person類的超類插入了繼承體系之中。你會說,“但是調用person的superclass方法傳回的是object而不是thingswithnames啊。”是的,這是因為:單例類是匿名而不可見的,是以super-class和class方法都會跳過它。是以,一個更精确的類體系也需要包含子產品,如圖2-2所示。

《Effective Ruby:改善Ruby程式的48條建議》一第6條:了解Ruby如何建構繼承體系

當每個子產品被類包含時,它會立即被插入繼承體系中包含它的類的上方,以後進先出(lifo)的方式。每個對象都通過變量superclass連接配接,像單向連結清單一樣。這唯一的結果就是,當ruby尋找一個方法時,它将以逆序通路每個子產品,最後包含的子產品最先通路到。很重要的一點是,子產品永遠不會重載類中的方法。因為子產品插入的位置是包含它的類的上方,而ruby總是會在向上檢查之前先檢查類本身。(好吧……這不是全部的事實。確定你閱讀了第35條,來看看ruby 2.0中的prepend方法是如何使其複雜化的。)

既然你已經适應了子產品和單例類間的互動,那麼我的責任是改變點什麼進而讓困難來得更猛烈(但可愛)一點:

《Effective Ruby:改善Ruby程式的48條建議》一第6條:了解Ruby如何建構繼承體系

如果你以前沒見過這種文法,你可能會有點困惑,但這其實很明确。上面的代碼定義了僅用于customer對象的一個方法。這個特定的方法在其他任何對象上都不能調用。你覺得ruby是怎樣實作它的呢?如果你指出單例類,那麼你說對了。事實上,這個方法被稱為單例方法。當ruby執行這段代碼時,它将建立一個單例類,将name方法作為其執行個體方法,随後将這個匿名類作為customer對象的類進行插入。不過即使此時customer對象的類是單例類,ruby的内省方法class方法仍将跳過它并傳回customer。這讓我們覺得難以了解,但讓ruby的實作變得簡單。當它查找name方法時,僅需要周遊類的結構就可以了。不需要特殊的邏輯。

這也透露了一些為什麼這種類被稱為單例類的線索。多數類服務很多對象,比如array類包含你的程式中用到的每個數組對象使用的方法。而單例類僅僅服務一個對象。

除了類方法以外,我沒有更多的花樣可以描述了。你将很高興地發現你可能已經了解它們了。它們是我們已經探索過的單例類的另一種表現形式。

注意,當你定義單例方法時,你指定了一個對象(可以認為是接收者),它将是這個對象的所有者。當你定義一個類方法時,你通過定義類對象做了完全相同的事情,不論是通過類名還是更常見的self變量。對象畢竟是對象,即使它是類。是以類方法也是作為單例類的執行個體方法存儲的。自己來看:

《Effective Ruby:改善Ruby程式的48條建議》一第6條:了解Ruby如何建構繼承體系

最後,當ruby希望查找一個方法時,隻需要從繼承體系着手。不論是執行個體方法還是類方法都是這樣。一旦你了解了ruby是如何操作繼承體系的,那麼了解查找方法的過程就非常簡單了:

1.?找到接收者的類,實際上它可能是一個隐藏的單例類。(記住,你在ruby代碼中不能這樣做,因為class方法會跳過單例類。你得直接調用singleton_class方法。)

2.?查找該類中存儲的執行個體方法清單。如果找到該方法就停止搜尋并執行代碼。否則,繼續第3步。

3.?順着繼承體系向上找到其超類并重複第2步。(這個超類也可能是另一個單例類。在ruby中,通過superclass方法,它可能是不可見的。)

4.?重複第2步和第3步直到ruby找到這個方法或者抵達體系的頂點。

5.?當抵達頂點時,ruby回到原來的接收者的第1步的地方繼續查找,不過這回找的是method_missing方法。

如你所見,當方法存儲于不可見的單例類時,在代碼中很難明确地看到整個繼承體系。但是你能使用ruby為了内省而提供的幾個方法來拼出完整的場景:

singleton_class方法傳回接收者的單例類,如果不存在就建立它。

ancestors方法傳回組成了繼承體系的所有類和子產品的數組。它隻能在類和子產品上被調用,并會跳過單例類。

included_modules方法傳回和ancestors方法一樣的數組,不過其中所有的類都被過濾掉了。

還好,你很少會使用這些方法。明白繼承體系的建構和内部搜尋原理已經足夠解決大多數情況了。如果需要的話,第35條包含了一些例子。

要點回顧

要尋找一個方法,ruby隻需要向上搜尋類體系。如果沒有找到這個方法,就從起點開始搜尋method_missing方法。

包含子產品時ruby會悄悄地建立單例類,并将其插入在繼承體系中包含它的類的上方。

單例方法(類方法和針對對象的方法)存儲于單例類中,它也會被插入繼承體

系中。