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

Posted zzulp

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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选项将由最相关的类决定。
如果两个槽确实是相同的,则上述合并策略将没有问题,但有时候这并不是你想要的,这就需要使用包系统来避免不相关代码中的名字冲突。

以上是关于Common Lisp学习之七:LISP的面向对象编程的主要内容,如果未能解决你的问题,请参考以下文章

Python学习之基础篇

Common Lisp : Lexical varible , Dynamic varible ——作用域,生存期 ——environment : 绑定, 闭包与共享对象

Common Lisp 中的本地状态

在 Common Lisp 中将 FUNCTION 转换为 STRING 或 SYMBOL

Emacs Lisp 和 Common Lisp 之间的主要区别是啥? [关闭]

Python学习之七面向对象高级编程——使用@property