《实用Common Lisp编程》第16-17章,面向对象细节补遗(2):广义函数与继承
上一节,我们测试了广义函数的三个主要的辅助函数 :around,:before 和 :after 的行为。
?
这次,我们来看看,广义函数在继承关系中的行为,以及特化对象与多重函数等。
?
广义函数与继承
?
从书中,我们知道,common lisp和其他常见的 oop 最大的不同是,common lisp的多态行为是用广义函数而不是常见的对象方法来实现的。
?
对一个广义函数来说,不同的对象可以通过对象实例进行特化,并分别实现这个广义函数,因此,不同的对象就此拥有了函数名相同但行为不同的对象。
?
另一方面,子类的广义函数实现,可以通过 call-next-method ,来调用父类的同名广义函数,从而实现一种“串联”的效果。
?
并且,方法是按照特殊程度排列的,越特殊(或者说,与调用对象越相似或相等)的方法越先被找到,而越泛化的方法越迟被找到。
?
总的来说,一个类自身的方法总是最先被找到,然后是父类的同名方法,父类的父类的同名方法,等等。
?
如果一个方法特化了一个以上的对象,我们称之为多重方法,这种方法的匹配更复杂一点,它按方法参数从左到右,以类似于单对象示例的方法匹配。
?
最后,如果一个对象有多个父类,那么按照继承列表从左到右开始,越左边的同名方法越特殊。
?
嗯,规则大概就是这样,如果文字描述让你有点头晕,我建议你还是先看看实例(其实我也一样。。。)。
?
?
类实现未定义的广义函数
?
书中说,我们可以不用 defgeneric 定义广义函数,而直接创建一个方法,那么广义函数的定义会被自动创建,我们这就来验证一下:
?
?测试:
(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 parameterThe following restarts are available:SKIP ? ? ? ? ? :R1 ? ? ?skip (DEFMETHOD GREET # ...)RETRY ? ? ? ? ?:R2 ? ? ?retry (DEFMETHOD GREET # ...)STOP ? ? ? ? ? :R3 ? ? ?stop loading file /tmp/t.lispABORT ? ? ? ? ?:R4 ? ? ?Abort main loop
噢噢,当我试图将文件载入解释器的时候,lisp就跟我抱怨起来了,看来果然不能用参数不同的同名方法阿。
使用 &key 、 &optional 、 &rest 使方法支持不同参数
看了上面的示例,你肯定有点伤心,因为如果方法不能支持不同参数的话,那么连 JAVA 的那种根据参数进行重载方法的小把戏我们在强大 common lisp 中居然就不可以做了。
嗯,而实际上,根据书本的第五章,我们可以在广义函数中使用 关键字方法 &key 、可选方法 &optional 或者 不定长方法 &rest ,来达到同名方法使用不同参数的效果。?这个定义有点儿复杂,让我来解释一下。
首先,我定义了一个广义函数 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 ,来达到让同名方法支持不同数量参数的目的。
多重方法
到目前为止,我们所有的方法都是特例化单个类实例来实现的,而实际上,特例化的类实例的数量并没有限制。
而特例化多于一个类实例的方法,称之为多重方法,这个特性非常之酷,让我们免去了写对象分派器的功夫,我们这就来试试:(greet (make-instance 'person))hello someoneNIL
而第二个 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)。?
1 楼 zx371323 2012-03-06 下一章 有木有?