上一节,我们测试了广义函数的三个主要的辅助函数 :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)。