`
huangz
  • 浏览: 320545 次
  • 性别: Icon_minigender_1
  • 来自: 广东-清远
社区版块
存档分类
最新评论

《实用Common Lisp编程》第16-17章,面向对象细节补遗(2):广义函数与继承

阅读更多

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

(defclass 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)。






 

分享到:
评论
1 楼 zx371323 2012-03-06  
下一章 有木有?

相关推荐

Global site tag (gtag.js) - Google Analytics