Common Lisp学习之七:LISP的面向对象编程

1 面向对象和Common Lisp
  面向对象的基本思想在于数据和操作的绑定-即封装,而更重要的是多态。CL和多数OO语言一样是基于类的,类通过层次结构组织在一起,形成了对象的分类系统。
  在CL中,所有类的基类是单根T,但它其同时支持多重继承。然而CL的面向对象是基于广义函数来实现的。广义函数类似于抽象函数,只定义接口(名字和参数列表),不提供实现。实现由方法提供,每个方法提供广义函数用于特定参数类型的实现。因此,基于广义函数的OO与基于消息传递的OO,最大的区别在于方法并不属于类,而是属于广义函数。由广义函数负责在一个特定的调用中检测哪些方法将被运行。
  方法通过特化由广义函数所定义的必须参数,来表达它们所处理的特定类型。可以由两种方式来特化参数。一种是提供类名;一种是提供基于EQL的对象实例化,此时方法将仅仅绑定个特定的实例对象上。


2 广义函数
2.1 定义广义函数
(defgeneric fun (arg1 arg2 ...)
  (:documentation ""))

defgeneric的形参列表指定了方法都必须接受的参数。在函数体上,必须带有:document选项,用来描述此广义函数的用途字符串。

2.2 定义方法
(defmethod fun ((arg1 type) (arg2 (eql *obj*)) ...)
   (body-form*))

方法的形参列表必须和广义函数保持一致,即带有相同数量的必要和可选参数,且必须接受对于的&rest或&key形参的参数。

2.3 方法组合
2.3.1 Call-next-method
类似于其他OO语言中调用基类上的同名函数。支持传递参数调用。

2.3.2 辅助方法与方法组合
以上介绍的是主方法的组合,此外CL还支持三种辅助方法::before,:after,:around。
(defmethod fun :before ((arg1 type) ...)
  (body-form*)) 

:before方法用于在任何主方法之前调用,:after用于在所有主方法之后调用;而如果存在:around方法,则调用之,直到在最不相关的around方法中通过call-next-method调用(:before:主方法:after)的组合体。

以上提到的:before/after/around为标准组合机制。在标准组合机制中,调用一个通用函数后的顺序是:
I  最具体的:around方法到最不相关的:around,如果有的话。返回值是:around方法的返回值
II 依次调用:before,从最具体到最不具体;最具体的主方法;所有:after方法,从最不具体到最具体。返回值是最具体主方法的返回值。

2.3.3 内置方法组合
除了标准组合以外,还有9种内置组合方法,即+ and or list append nconc min max和progn。

与call-next-method的链式调用方法不同,内置组合方法将在所有主方法结果上应该组合方法。例如+将算出所有主方法返回结果的和。 使用内置方法的广义函数:
(defgeneric fun (var)
  (:documentation "")
  (:method-combination +))

默认情况下,所有这些方法组合以最相关者优先的顺序组合主方法。但可以使用关键字:most-specific-last来逆转这一顺序。逆转并不影响:around方法。 随后,可以定义使用了对应的组合方法的广义函数的主方法
(defmethod fun + ((var) ...)
  (...))

内置方法组合也支持:around方法,其工作方式与标准方法组合的:around类似:最相关的先运行,call-next-method用于将控制传递到越来越不相关的方法,直到达到组合的主方法。


2.4 多重方法
显式地特化了超过一个广义函数中必要参数的方法称为多重方法。多重方法无法存在于消息传递系统的OO语言中,因为多重方法并不属于某个特定的类。

多重方法并不能处理组合爆炸的问题,例如有5种类型,6种操作,那么无论如何会有30种组合,需要实现30个方法。visitor模式也无法解决这个问题。多重方法的好处是我们不必实现复杂的多重分发的代码。

当一个通用函数被调用时,参数决定了一个或多个可用方法。如果有多个可用,最具体的将会被调用。

最具体可用的方法由调用传入参数所属类型的优先级决定(参见第3节中的多重继承优先级)。如果一个可用方法的第一个参数特化后,其类的优先级高于其他可用方法的第一个参数,则此方法便是最具体的。平手时比较第二个参数,以此类推。需要注意的是,使用EQL特化的方法比用类特化的优先级高。

需要注意的是只有必要参数才可以特化。

可以认为消息传递模型只是广义函数只有一个参数时的特定情况。

3 类
3.1 定义类
(defclass name (superclass-name*)
  (slot-spec*))

如果未指定基类,则默认继承自standard-object子类。另外类包、函数名和变量名位于不同的名字空间里,因而类、函数和变量名可以相同。
类的字面表示是#<...>
(defclass person ()
  (name
   sexy))

(setf p (make-instance 'person))
(setf (slot-value p 'name) "Jobs")
(setf (slot-value p 'sexy) "male")

3.3 对象初始化
定义类是可以指定成员的初始化参数和默认值。
(defclass person ()
  ((name :initarg :pname :initform "")
   (age :initarg :page  :initform 0)))

initform可以是语句。需要注意的是initarg的优先级比initform高。如果make-instance提供了参数,则忽略默认值。

此外还可以为initialize-instance定义一个after方法,可以用来在一个对象初始化后执行一些操作。

3.4 访问器
defclass支持两个选项:reader和:writer来定义读/写访问器,以避免我们使用slot-value编写重复的代码。
(defclass person () 
	   ((name :initarg :pname :initform (error "don't have name") :reader name)
	    (age  :initarg :page  :initform 0 :reader age :writer (setf age))
            (sexy :accessor sexy)))

如果一个slot即读又写,则可以使用:accessor来说明一个slot。
(setf *p* (make-instance 'person :pname "Jobs" :page 66))
(name *p*)  #jobs
(age *p*)   #66

此外:documentation也可应用在slot里,用来说明一个slot的用途。

即便有了访问器,有时仍会比较麻烦,而with-slots和with-accessor宏提供了一个代码块,用来绑定访问类的slot,从而便我们不必须对每个slot都定义访问器。
(defmethod func ((obj type) ...)
  (with-slots (slot*) obj
    body-form*))

slot*有两种形式,一种是槽的名字,另一种是一个两元素列表:首元素为槽别名,末元素为槽名。

如果槽上已经有了:accessor,则可以使用with-accessors。其使用和with-slots相同,不同的是其slot*每一项必须是包含了两元素的列表。

3.5 槽的分配方式与类型
slot支持:allocation的选择,可以指定为:class或:instance,如果指定为:class,则所有实例共享,反之各个对象均私有。默认是instance的,所以不需要特别声明。
这有点类似于C类语言中的静态成员变量,但CL无法在没有对象实例时访问:class成员,所以有一定的区别。

:type可以指定一个类型,这样slot里只能存储指定的类型。

3.6 槽与继承
3.6.1 多重继承优先级
假设有如下的继承层次:

要替一个类别建构一个这样的网络,从最底层用一个节点表示该类别开始。接著替类别最近的基类画上节点,其顺序根据  defclass 调用里的顺序由左至右画,再来给每个节点重复这个过程,直到你抵达一个类别,这个类别最近的基类是  standard-object ── 即传给  defclass 的第二个参数为  () 的类别。最后从这些类别往上建立链接,到表示  standard-object 节点为止,接著往上加一个表示类别  t 的节点与一个链接。结果会是一个网络,最顶与最下层各为一个点,如上图所示。

一个类别的优先级列表可以通过如下步骤,遍历对应的网络计算出来:

  1. 从网络的底部开始。
  2. 往上走,遇到未探索的分支永远选最左边。
  3. 如果你将进入一个节点,你发现此节点右边也有一条路同样进入该节点时,则从该节点退后,重走刚刚的老路,直到回到一个节点,这个节点上有尚未探索的路径。接著返回步骤 2。
  4. 当你抵达表示 t 的节点时,遍历就结束了。你第一次进入每个节点的顺序就决定了节点在优先级列表的顺序。

这个定义的结果之一(实际上讲的是规则 3)在优先级列表里,类别不会在其子类别出现前出现。

3.6.2 槽的合并
派生类可以继承基类的槽。这样,一个类可能会有多个同名的槽。CL的解决方方式是将来自所有继承层次的同名描述符合并在一起,并为唯一的槽名创建单一的描述符。

在合并时,不同的槽选项会有不同的处理方法。
:initform将使用来自最相关类的那一个。派生类可以指定自己的initform,这样可以覆盖基类的初始值。
:initargs将全部可用,使用任意一个都是可行的。如果创建实例时,使用了多个关键字参数初始化同一个槽,则使用第一个参数的值。
:read :write :accessor选项不会包含在合并的槽中,因为基类的方法已经可用在新类上。不过新类也可提供自己的访问器。
:allocation选项将由最相关的类决定。

如果两个槽确实是相同的,则上述合并策略将没有问题,但有时候这并不是你想要的,这就需要使用包系统来避免不相关代码中的名字冲突。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值