`
huangz
  • 浏览: 322217 次
  • 性别: 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  
下一章 有木有?

相关推荐

    实用Common.Lisp编程.pdf

    Common Lisp中的Common Lisp Object System (CLOS)提供了多重继承和多态性的支持,使得面向对象编程在Common Lisp中成为可能。 #### 5. 跨平台性 Common Lisp编写的程序可以在多种操作系统上运行,包括Windows、...

    实用Commonlisp编程

    4. 面向对象编程(CLOS):Common Lisp支持多种编程范式,其中面向对象编程是通过Common Lisp对象系统(Common Lisp Object System,简称CLOS)实现的。CLOS不仅支持类和继承,还支持多重方法分派、组合设计模式等...

    实用Common Lisp编程

    本书首先从作者的学习经过及语言历史出发,随后用21个章节讲述了各种基础知识,主要包括:REPL 及Common Lisp 的各种实现、S- 表达式、函数与变量、标准宏与自定义宏、数字与字符以及字符串、集合与向量、列表处理、...

    实用common lisp 编程

    - **面向对象编程**:Common Lisp支持面向对象编程(OOP),尽管它的OOP机制与Java或C++等语言有所不同。Common Lisp的面向对象系统称为CLOS(Common Lisp Object System),它允许程序员创建类和实例,并实现多态性...

    实用Common.Lisp编程

    这本《Practical Common Lisp》之所以号称Practical,正是因为这本书大量介绍Common Lisp在现实世界中的各种应用方式,算是第一本「入世传教」的Common Lisp著作。《Practical Common Lisp》是目前最畅销的Common ...

    Common Lisp 高级编程技术

    1. **Common Lisp基础**:Common Lisp是一种通用的多范式编程语言,支持过程式、面向对象、函数式和反射等多种编程风格。书中会介绍其基本语法,包括S表达式、读取-求值-打印循环(REPL)以及Lisp代码的读取和评估机制...

    Practical Common Lisp-1st-2005

    《Practical Common Lisp-1st-2005》是一本专注于Common Lisp编程语言的实用书籍,作者Peter Seibel通过这本书向读者展示了如何使用Common Lisp来解决真实世界中的问题,强调程序员作为工程师和艺术家的双重身份,而...

    lisp基础教程

    - **定义与背景**:Common Lisp是一种功能强大的多范式编程语言,是Lisp家族的一员,具有丰富的历史和深厚的社区支持。作为一种标准化的Lisp方言,它由多个Lisp方言合并而成,旨在为程序员提供一个统一且强大的开发...

    ANSI Common Lisp 中文翻译版.pdf

    该资源包含了 17 章节,从基础的列表、特殊数据结构、控制流程、函数、输入与输出、符号、数字、宏、Common Lisp 对象系统、结构、速度、进阶议题到高级主题的推论、生成 HTML、对象等。 ANSI Common Lisp 是一种...

    常用Common LISP函数分类总结.docx

    在 Common Lisp 中,数据结构的访问和处理是编程的基础,涉及到各种函数,这些函数用于创建、检查、修改和操作Lisp的数据结构。本篇将详细阐述一些常用的Lisp函数,以便于理解和应用。 1. 数据结构的访问和处理函数...

    实用Common Lisp编程 田春

    个章节讲述了各种基础知识,主要包括:REPL 及Common Lisp 的各种实现、S- 表达式、函数与变量、标 准宏与自定义宏、数字与字符以及字符串、集合与向量、列表处理、文件与文件I/O 处理、类、FORMAT 格式、符号与包,...

    Common Lisp 编程

    - **函数式编程**:Lisp鼓励函数式编程风格,函数是第一类对象,可以作为参数传递,也可以作为返回值。 2. **Common Lisp简介** - **标准统一**:与其他Lisp方言相比,Common Lisp是标准化的,有ANSI Common Lisp...

    Common Lisp-对符号计算的温和介绍Common Lisp - A Gentle Introduction To Symbolic Computation

    它不仅具有强大的功能性和灵活性,而且还支持多种编程范式,如过程式、函数式、面向对象等。Common Lisp的设计哲学是将简单性、可扩展性以及实用性相结合,使得开发者能够高效地编写出高性能的应用程序。 #### 三、...

    ANSI Common Lisp 中文版

    12. **第11章:Common Lisp对象系统(CLOS)**: - 详细分析了CLOS这一面向对象编程框架的原理和用法。 - 通过具体案例展示了如何利用CLOS来构建复杂的应用程序架构。 13. **第12章:结构(Structure)**: - ...

    ANSI Common Lisp 中文翻译版

    - **第十一章:Common Lisp 对象系统(CLOS)**:讲述了 Lisp 中的对象系统 CLOS,这是 Lisp 中实现面向对象编程的主要方式。 - **第十二章:结构(Structure)**:讲解了 Lisp 中用于定义固定字段记录的数据类型...

Global site tag (gtag.js) - Google Analytics