天天看點

《Effective Ruby:改善Ruby程式的48條建議》一第7條:了解super的不同行為

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

假設你已經寫好了一個類,這個類繼承自一個基類。而被繼承的類定義了一個不那麼适合新類的方法。是以你決定改進那個方法,但是你不想完全替代既有方法,因為它承擔了90%的必要且繁重工作。你也不想改變基類,因為那将破壞其他代碼的功能。先忽略掉使用組合而非繼承的這個更好的選項,讓我們費點力氣來重載這個方法。你最後做了這些改動:

《Effective Ruby:改善Ruby程式的48條建議》一第7條:了解super的不同行為
《Effective Ruby:改善Ruby程式的48條建議》一第7條:了解super的不同行為

你讓自己陷入了困境。最大的問題是:你如何調用超類中的m1方法?如果你試圖調用衍生類中的方法,你将陷入無限循環中。那沒什麼幫助。這就到了super出場的時候了:

《Effective Ruby:改善Ruby程式的48條建議》一第7條:了解super的不同行為

現在我們有些眉目了。新版的m1方法使用super來調用父類的m1方法。本例中,super有點替代base#m1的意思。你在能夠直接使用base#m1的任何地方都能使用super。這包括通過super傳遞你希望傳遞給base類的m1的任何參數。(當然僅限于目标方法能夠接收的那些參數。)小心這個陷阱。

即使super被用作并扮演得像個方法,它其實是ruby的關鍵字。這之間的不同很重要,因為super是基于ruby中設定為可選的元素——括号,來改變行為的。我說得很嚴肅哦,請仔細想上一秒。當使用super時,如果省略了括号是會改變其行為的。這比聽上去還要嚴重。要知道括号改變了什麼,我們需要回顧一下super的三種寫法:

super的第一種用法最為平常——就是我們之前所見的那樣。如果你至少給super一個參數,那麼它的行為就好像正常的ruby方法,并且括号是可選的。通過這種形式,super将參數全部傳遞給目标方法。

如果沒有給super任何參數和括号,它的行為可能會和你的預期不同。這種用法之下,super在調用目标方法時将宿主方法(enclosing method)的所有參數全部傳遞過去。如果存在方法塊它也會一并傳遞。

你應該可以看出,這種形式下的super會帶來一些副作用。首先,這樣使用super僅僅在目标方法和宿主方法接收相同數量的參數時才可用。比如,這對我們之前看到的例子中的方法base#m1和derived#m1就不适用。試圖在derived#m1方法中使用無參的super會引發一個argumenterror異常。如果被重載的方法不接收參數而宿主方法接收,那麼着同樣會為你帶來一個相同的異常。

可以看出的第二件事與傳遞給重載方法的參數的值有關。如果你在使用無參形式調用super之前,在宿主方法中改變了其參數中的一個值,那麼super會将改變後的目前值傳遞給目标方法,而非原始的值。這看起來非常合理,但有時你也需要注意。

當你希望在使用super時不将任何參數傳遞給重載方法,你需要使用空括号,即super()。在ruby中這種形式看起來實在很怪。即使我們喜歡使用括号,在無參調用時也不會加上它們。這種super的用法看起來有些不自然,但這是無參(也沒有塊)調用重載方法的唯一途徑。

以下是對上述規則的代碼表述:

《Effective Ruby:改善Ruby程式的48條建議》一第7條:了解super的不同行為

下一個需要關心的是super如何尋找要重載的方法。你也許會簡單地想,隻要調用其超類的同一方法就可以了,但這也簡單地過分了哦。事實上,super是從整個繼承體系中尋找的,見第6條,不過有些許微小的差異。使用super時,它從繼承體系的上一層去尋找和目前方法同名的方法。是以,它将從目前類的上一層開始尋找。

如果你還記得我們對繼承體系的研究,你将明白上一層可能會是一個單例類。這很棒啊,因為這意味着super能夠對其引用的子產品中的方法進行重載。

《Effective Ruby:改善Ruby程式的48條建議》一第7條:了解super的不同行為

這個例子也指出了使用super的一個限制。如果你希望調用一個定義在超類中的方法,而與此同時如果包含的子產品中也定義了同名方法,super就無法幫你達到目的。顯然,super會在找到第一個比對的同名方法後停下來,而那是包含的子產品中的方法,而不是超類中的。但如果你真的意外遇到了這種情況,那麼可能在設計上存在嚴重的問題。可以考慮使用組合而非繼承了。

最後一個需要警示的是super是如何與method_missing互相作用的。如果你思考過這個問題,你會知道,隻要在繼承體系中存在另一個同名方法,使用super就不會引發method_missing。另一方面,如果你恰巧是一個瞌睡的開發者或者正在使用一些元程式設計的黑魔法,你最終可能會在使用super時引發method_missing。如果是這樣,你讀到這節會非常開心。

就像一個普通的方法查找一樣,如果super無法找到它要尋找的方法,将調用method_missing來報告這個錯誤。如你料想的那樣,ruby尋找method_missing時會從原接收者所在的類開始查找。這一切都很好,我們還可以從異常結果中得到有用的錯誤資訊。

《Effective Ruby:改善Ruby程式的48條建議》一第7條:了解super的不同行為

ruby會在内部追蹤method_missing方法是否由一個錯誤的super調用引發,若是如此,會在異常中給出一些額外的資訊。但這僅發生在ruby沒能在繼承體系中找到已定義的method_missing方法而使用預設實作時。一旦繼承體系中存在任何定義了method_missing方法的類,你就會丢失這個有用的細節。這個資訊無法找回,即使你在自己的method_missing實作中調用super也不行。

再多說點,method_missing在被調用時描述的細節是說,方法是存在的,隻是它在錯誤的地方。如果上面的superhappy類定義了method_missing方法,它将在調用laugh時被調用,因為那個方法試圖使用super調用另一個不存在的laugh方法。那麼method_missing的第一個參數是什麼呢?對的,就是缺少的方法的名字,本例中就是:laugh。但是superhappy本身是有laugh方法的。有點迷茫了是不是?

假如你想細數避免自行定義method_missing方法的原因,請確定你也算上了這個(包括第30條裡提到的)。本例中,來自method_missing方法的預設實作中的nomethoderror異常顯然比我們自己定義的method_missing方法更好。

要點回顧

當你想重載繼承體系中的一個方法時,關鍵字super可以幫你調用它。

不加括号地無參調用super等價于将宿主方法的所有參數傳遞給要調用的方法。

如果希望使用super并且不向重載方法傳遞任何參數,必須使用空括号,即super()。

當super調用失敗時,自定義的method_missing方法将丢棄一些有用的資訊。在第30條中有method_missing的替代解決方案。