上一節,我們測試了廣義函數的三個主要的輔助函數 :around,:before 和 :after 的行爲。
這次,我們來看看,廣義函數在繼承關係中的行爲,以及特化對象與多重函數等。
廣義函數與繼承
從書中,我們知道,common lisp和其他常見的 oop 最大的不同是,common lisp的多態行爲是用廣義函數而不是常見的對象方法來實現的。
對一個廣義函數來說,不同的對象可以通過對象實例進行特化,並分別實現這個廣義函數,因此,不同的對象就此擁有了函數名相同但行爲不同的對象。
另一方面,子類的廣義函數實現,可以通過 call-next-method ,來調用父類的同名廣義函數,從而實現一種“串聯”的效果。
並且,方法是按照特殊程度排列的,越特殊(或者說,與調用對象越相似或相等)的方法越先被找到,而越泛化的方法越遲被找到。
總的來說,一個類自身的方法總是最先被找到,然後是父類的同名方法,父類的父類的同名方法,等等。
如果一個方法特化了一個以上的對象,我們稱之爲多重方法,這種方法的匹配更復雜一點,它按方法參數從左到右,以類似於單對象示例的方法匹配。
最後,如果一個對象有多個父類,那麼按照繼承列表從左到右開始,越左邊的同名方法越特殊。
嗯,規則大概就是這樣,如果文字描述讓你有點頭暈,我建議你還是先看看實例(其實我也一樣。。。)。
類實現未定義的廣義函數
書中說,我們可以不用 defgeneric 定義廣義函數,而直接創建一個方法,那麼廣義函數的定義會被自動創建,我們這就來驗證一下:
(defclass person ()
())
(defmethod greet ((people person))
(format t "greet ~%"))
測試:
(greet (make-instance 'person))
greet
NIL
嗯,沒有報錯,看來真的可以。
子類調用父類的方法
這次,我們來試試,當子類調用一個自己不存在的方法,而其父類擁有該方法的時候,會發生什麼事情。
如果一切正常的話,父類的方法應該會被調用。
(defclass animal ()
())
(defmethod greet ((people animal))
(format t "animal's greet method ~%"))
(defclass person (animal)
())
測試:
(greet (make-instance 'person))
animal's greet method
NIL
嗯,的確是父類的方法被調用了。
父類調用子類擁有的方法
現在,我們將之前的關係掉轉,如果一個父類沒有方法 greet ,而子類有的話,會發生什麼事?
嗯。。。我猜會出現一個錯誤,因爲類型從來都是向上轉而不是向下轉的,但是我還是想試一試:
(defclass animal ()
())
(defclass person (animal)
())
(defmethod greet ((people person))
(format t "person's greet method ~%"))
測試:
(greet (make-instance 'animal))
*** - NO-APPLICABLE-METHOD: When calling #<STANDARD-GENERIC-FUNCTION GREET>
with arguments (#<ANIMAL #x21B1854E>), no method is applicable.
The following restarts are available:
RETRY :R1 try calling GREET again
RETURN :R2 specify return values
ABORT :R3 Abort main loop
噢噢,果然如我所料,沒有可應用的方法。
注意上面的代碼和之前的代碼的細微區別:第一,我們將 greet 改成了 person 類的特化方法。另外,我們在測試的時候,生成的是一個 animal 實例,而不是之前常用的 person 實例。
子類和父類擁有同名方法
嗯,探險完畢,我們現在來做些正常點的事情。
當子類和父類擁有同名方法的時候,如果子類調用方法,那麼子類自己的方法就比父類的同名方法更特殊,因此,子類自己的方法就會被調用。
按理來說是這樣,嗯,讓我們實際試試:
(defclass animal ()
())
(defmethod greet ((people animal))
(format t "animal's greet method ~%"))
(defclass person (animal)
())
(defmethod greet ((people person))
(format t "person's greet method ~%"))
測試:
(greet (make-instance 'person))
person's greet method
NIL
嗯,再來試試調用父類:
(greet (make-instance 'animal))
animal's greet method
NIL
一切正常,我們可以看見,父子兩個類中的 greet 方法互不影響。
子類方法通過調用call-next-method調用父類的同名方法
在上面,我們看到父子兩個類擁有分別調用的 greet 方法,它們互不相關。
但是,當上面的情況出現時,也即是說,父類和子類擁有同名的方法,那麼這時,我們可以在子類的同名方法中,通過調用 call-next-method 方法,來調用父類的同名方法,達到組合兩個方法來使用的效果。
(defclass animal ()
())
(defmethod greet ((people animal))
(format t "animal's greet method ~%"))
(defclass person (animal)
())
(defmethod greet ((people person))
(format t "person's greet method ~%")
(call-next-method)) ; call animal's greet method
試試:
(greet (make-instance 'person))
person's greet method
animal's greet method
NIL
從上面的測試可以看到,person 先調用自己的 greet 方法,然後通過調用 call-next-method 方法,將執行權轉給了父類的 greet 方法,從而調用了父類的 greet 方法。
call-next-method 並沒有限制我們只能調用兩個 greet 方法,換言之,如果 animal 還有一個父類,這個父類還有一個 greet 方法,我們同樣可以在 animal 的 greet 方法中通過 call-next-method 來調用這個 greet 方法,以此類推。
另外,子類的 greet 方法的 call-next-method 並不會影響到父類的行爲:
(greet (make-instance 'animal))
animal's greet method
NIL
可以看到,animal 類的 greet 方法的行爲沒有改變。
兩個廣義函數的實現擁有不同的參數
書上說,每個廣義函數的實現,也即是,同名的方法,只能有相同的參數,我們這就來試試它是不是真的。
我定義一個廣義函數 greet ,然後定義兩個方法,它們一個不接收除對象實例外的其他參數,另一個則接受一個 name 參數:
(defclass a ()
())
(defmethod greet ((obj a))
(format t "a's greet ~%"))
(defclass b ()
())
(defmethod greet ((obj b) name)
(format t "b's greet call by ~a ~%" name))
測試:
(load "t")
;; Loading file /tmp/t.lisp ...
*** - #<STANDARD-METHOD (#<STANDARD-CLASS B> #<BUILT-IN-CLASS T>)> has 2, but
#<STANDARD-GENERIC-FUNCTION GREET> has 1 required parameter
The following restarts are available:
SKIP :R1 skip (DEFMETHOD GREET # ...)
RETRY :R2 retry (DEFMETHOD GREET # ...)
STOP :R3 stop loading file /tmp/t.lisp
ABORT :R4 Abort main loop
噢噢,當我試圖將文件載入解釋器的時候,lisp就跟我抱怨起來了,看來果然不能用參數不同的同名方法阿。
使用 &key 、 &optional 、 &rest 使方法支持不同參數
看了上面的示例,你肯定有點傷心,因爲如果方法不能支持不同參數的話,那麼連 JAVA 的那種根據參數進行重載方法的小把戲我們在強大 common lisp 中居然就不可以做了。
嗯,而實際上,根據書本的第五章,我們可以在廣義函數中使用 關鍵字方法 &key 、可選方法 &optional 或者 不定長方法 &rest ,來達到同名方法使用不同參數的效果。
(defgeneric greet (obj &key))
(defclass a ()
())
(defmethod greet ((obj a) &key)
(format t "a's greet ~%"))
(defclass b ()
())
(defmethod greet ((obj b) &key name)
(format t "b's greet call by ~a ~%" name))
這個定義有點兒複雜,讓我來解釋一下。
首先,我定義了一個廣義函數 greet,它接受一個 obj 參數,以及一個關鍵字參數列表,但我沒有指名關鍵字參數列表裏面的關鍵字。
然後,在 a 類的 greet 方法中,我同樣在定義 greet 方法中放置了一個空的關鍵字列表 &key ,我只是聲明一個關鍵字列表,但沒有指定任何關鍵字,也即是,實際上,這個 greet 方法只接受 obj 一個參數。
最後,在 b 類的 greet 方法中,我定義了特化成 b 示例的 obj 參數,以及,一個名字爲 name 的關鍵字參數,也即是說,這個 greet 方法可以接受兩個參數,一個 obj,一個 name。
嗯,解釋得差不多了,是時候測試一下了:
(greet (make-instance 'a))
a's greet
NIL
我先試了類 a 的 greet ,它如我們意料之中所想的那樣,只接受一個參數就可運行。
好,接下來,我們試試類 b 的 greet 方法,如果一切正常的話,它應該接受兩個參數,並且第二個參數要聲明爲 :name :
(greet (make-instance 'b) :name "huangz")
b's greet call by huangz
NIL
嗯,看上去不錯,這樣一來,我們就成功地讓同一個廣義函數的不同方法支持不同的參數了。
最後,值得一提的是,不單單是關鍵字參數,我們還可以通過可選參數 &optional 和 不定量參數 &rest ,來達到讓同名方法支持不同數量參數的目的。
多重方法
到目前爲止,我們所有的方法都是特例化單個類實例來實現的,而實際上,特例化的類實例的數量並沒有限制。
而特例化多於一個類實例的方法,稱之爲多重方法,這個特性非常之酷,讓我們免去了寫對象分派器的功夫,我們這就來試試:
(defgeneric greet (one two))
(defclass a () ())
(defclass b () ())
(defclass c () ())
(defclass d () ())
(defmethod greet ((one a) (two b))
(format t "hello a and b ~%"))
(defmethod greet ((one c) (two d))
(format t "halo c N d ~%"))
上面的代碼裏,我們定義了一個廣義函數 greet ,它接受兩個參數 one 和 two。
接着,我們定義了 a、b、c、d四個方法(如果你喜歡的話,也可以把它們想做東南西北、或者梅蘭竹菊什麼的。。。)
然後,我們定義了兩個 greet 方法,這兩個 greet 方法,根據接受的對象的示例的不同,它們產生的結果也不同。
對於第一個 greet 方法,它接受一個 a 類的對象和 b 類的對象,並打印一個普通的英語問候對白:
(greet (make-instance 'a) (make-instance 'b))
hello a and b
NIL
而第二個 greet 方法,則接受 c 類的對象和 d 類的對象,並打印一個有搖滾風格的問候對白(嗯,我知道,這根搖滾的關係似乎不大。。。):
(greet (make-instance 'c) (make-instance 'd))
halo c N d
NIL
嗯,兩個方法都運行得很好,這時一個疑問可能出現在你的腦海中,如果我用一個 a 對象 和 c 對象作爲參數調用 greet 方法,結果會怎麼樣?
這就來試試:
(greet (make-instance 'a) (make-instance 'c))
*** - NO-APPLICABLE-METHOD: When calling #<STANDARD-GENERIC-FUNCTION GREET>
with arguments (#<A #x21DA86DE> #<C #x21DA86EE>), no method is
applicable.
The following restarts are available:
RETRY :R1 try calling GREET again
RETURN :R2 specify return values
ABORT :R3 Abort main loop
噢,一個錯誤產生了,實際上,這不太算一個嚴重的錯誤,它出錯的原因是我們沒有定義符合 a 類對象和 c 類對象使用的 greet 方法。
再進一步將,你可以爲任何類對象的組合定義不同的方法,比如這裏的四個類 a、b、c、d 就一共有二的四次方(2^4)種不同的 greet 方法可供定義,如果你有五個類,就是 2^5 ,如果你有六個,就是 2^6 。。。
多繼承方法
另一個到目前爲止,我們的方法都是基於單個父類進行的,如果說,一個子類有多個同名方法,那麼將產生怎麼結果。
按照書上的說法,多個父類的同名方法的特殊程度按照它們被子類繼承的順序,從左到右,越靠左邊的越先被找到。
也即是,如果我們用一個類 a ,它繼承了 b 和 c 方法,定義如下:
那麼當類 a 的實例尋找一個不存在於類 a 中的方法時,它會先查找 b,再查找 c。
嗯,規則就是這樣,我們來詳細試試:
(defgeneric greet (people))
(defclass b ()
())
(defmethod greet ((people b))
(format t "b's greet ~%"))
(defclass c ()
())
(defmethod greet ((people c))
(format t "c's greet ~%"))
(defclass a (b c)
())
運行試試:
(greet (make-instance 'a))
b's greet
NIL
如我們所料,b 類的 greet 方法被調用了,因爲它在 a 繼承列表的左邊,先於類 c ,因此它的 greet 方法先於類 c 的 greet 方法被找到。
基於EQL特化符來特化一個特定於某個對象的方法
最後一個到目前爲止,我們的方法的特化都是基於類進行的,也就是,對整個類的所有對象實例來說,都產生相同的行爲。
比如在以下程序中,無論你怎麼調用 greet 方法,它總是單調地打印出同一句話:
(defgeneric greet (people))
(defclass person ()
())
(defmethod greet ((people person))
(format t "hello someone"))
它的執行結果如下:
[2]> (greet (make-instance 'person))
hello someone
NIL
[3]> (greet (make-instance 'person))
hello someone
NIL
[4]> (greet (make-instance 'person))
hello someone
NIL
嗯,這個程序實在太乏味了,就像網上那些弱智的人工智能測試一樣可笑——那些呆呆的機器人,無論你重複問它多少遍 hello ,它都只回復你同一句話,唉。
而實際上,你可以根據一個EQL特化符,來讓某個方法對一個特定的對象產生特殊的行爲:
(defgeneric greet (people))
(defclass person ()
())
(defmethod greet ((people person))
(format t "hello someone"))
(defvar *huangz* :huangz)
(defmethod greet ((people (eql *huangz*)))
(format t "hello, huangz!"))
(defvar *admin* (make-instance 'person))
(defmethod greet ((people (eql *admin*)))
(format t "welcome back, master!"))
注意看代碼,我們定義了三個 greet 方法,兩個全局變量。
第一個 greet 爲所有不屬於 *huangz* 和 *admin* 的其他 person 類的實例服務,作出一般迴應 “hello someone" 。
(greet (make-instance 'person))
hello someone
NIL
而第二個 greet 方法,則使用 eql 操作符特例化了全局變量 *huangz* ,當我用 *huangz* 變量作爲參數傳給 greet 方法的時候,它會打印一條熱情的信息給我:
(greet *huangz*)
hello, huangz!
NIL
而第三個 greet 方法,則更科幻一點,當它遇到全局變量 *admin* 的時候,它會打印一條相當親切而忠心的信息:
(greet *admin*)
welcome back, master!
NIL
還有兩點(哦不,三點)細節要注意:
首先,eql 特化符使用的對象可以是任何對象的實例(也即是,任何類型),比如 *huangz* 就是一個關鍵字符號,而 *admin* 則是一個 person 對象的實例。
其次,你看到,要使用 eql 特化符特化對象,被特化的對象必須先被定義出來。
最後,eql 特化符和類特化符可以配合使用,比如你可以拓展爲 huangz 特化的 greet 方法的參數,讓他多接受一個 weather 參數,當下雨的時候,就對 huangz 打印下雨的消息,而當天氣晴朗的時候,就對huangz 打印天氣晴朗的消息,諸如此類。
這種組合特化符的特性非常強大,你可以慢慢研究,只要你有多幾個參數,你就可以搗鼓出寫不完那麼多的特化方法出來(還記得2^n嗎)。。。。
小結
嗯,這一章,我們詳細實驗了 common lisp 中關於廣義函數在繼承情況下的種種表現,並瞭解到特化符操作的強大和蛋疼之處。
下一章,我們再來看看, common lisp 如何在繼承中,處理對象的槽(slot)。