通用Lisp/高级主题/CLOS
通用Lisp对象系统 (CLOS) 是通用Lisp的一部分,它允许在程序中使用面向对象编程技术。它定义了诸如对象、类和方法以及它们之间的交互等概念。CLOS 是任何编程语言中最强大的对象系统,掌握其更奇特的方面可能需要时间。幸运的是,不必成为 CLOS 专家就能使用它。
CLOS 的两个正交概念是类和泛型函数。让我们从第一个开始...
类是一个描述其实例的结构和行为的“模板”。每种Lisp数据都是某个类的实例。有一些内置类,比如整数类或字符串类。可以使用class-of 函数来确定某个Lisp对象的类。
(class-of 5) => #<BUILT-IN-CLASS INTEGER> (class-of "aaaa") => #<BUILT-IN-CLASS STRING>
结果可能在您的特定实现中以不同的方式打印,但想法是一样的:#< >
是Lisp语法,表示不可读数据,这并不意味着它对人类不可读,而是对Lisp阅读器不可读。很容易确定 5 是内置类integer 的实例,而 "aaaa" 是内置类string 的实例。
内置类通常不是很有趣。但是,有一种方法可以创建用户定义的类,并且可以通过defclass 宏来实现。用户定义类的每个实例都将具有一定数量的槽,这些槽可以包含各种Lisp数据。让我们看看 defclass 的典型用法
(defclass book () ((author :initarg :author :initform "" :accessor author) (title :initarg :title :initform "" :accessor title) (year :initarg :year :initform 0 :accessor year)) (:documentation "Describes a book"))
它具有以下结构
(defclass name (superclasses) (slots) options)
我们现在将忽略超类,让我们看一下槽。每个槽都有一个名称和几个附加到它的选项。这些选项是
:initarg
- 一个关键字,它将在创建类实例时用于提供槽值。:initform
- 如果没有为槽提供值,它将使用 initform 的计算结果进行初始化。如果没有 initform,将发出错误信号。:reader
- 指定一个函数来读取特定槽。:reader aaa
表示:创建一个函数aaa,以便 (aaa instance) 返回槽的值。:writer
- 指定一个函数来写入特定槽。:writer bbb
表示:创建一个函数bbb,以便 (bbb value instance) 将实例的槽设置为 value。:accessor
- 指定一个函数来读取和写入槽的值。:accessor foo
表示:创建一个函数foo 并创建一个函数(setf foo),以便 (foo instance) 读取槽的值,而 (setf (foo instance) value) 设置槽的值。- 注意:
:reader foo :writer foo
是错误的!读者和作者需要不同的名称。请改用:accessor foo
。
- 注意:
:documentation
- 为特定槽提供文档字符串。
每个选项都是完全可选的,但至少应该提供:initarg
或:initform
中的一个,以便能够在对象创建时初始化槽。未能做到这一点会导致运行时错误。
至于类选项,唯一有用的选项是:documentation
,它为整个类提供文档字符串。
现在我们有了类,我们可以创建一些它的实例。Lisp 中的一切都是对象,但标准类的实例(如上面由defclass 定义的类)被称为标准对象。在本节的剩余部分,对象一词指的是标准对象。
如何创建一个新对象?我们需要一个函数make-instance
(setf *my-book* (make-instance 'book))
现在 *my-book*' 的值为类book 的一个对象。make-instance 的第一个参数可以计算为类本身(例如class-of 调用结果)或命名类的符号(例如引用此符号的结果)。在这种情况下,我们将使用符号,这更容易生成。
让我们看看 *my-book*
(class-of *my-book*) => #<STANDARD-CLASS BOOK> (author *my-book*) => "" (year *my-book*) => 0
这本书现在相当平淡。它的字段被设置为默认的类值,但很容易更改它们
(setf (title *my-book*) "ANSI Common Lisp") (setf (author *my-book*) "Paul Graham") (setf (year *my-book*) 1995)
这是因为在类定义中设置了适当的访问器函数。在一般情况下,访问对象的槽比较困难。例如,如果只定义了读取器函数,您仍然可以使用slot-value 函数来更改槽
(setf (slot-value *my-book* 'year) 1995) (year *my-book*) => 1995
您也可以丢弃读取器函数,并使用slot-value 函数来读取槽值。但是,这不会增加代码的可读性。
大多数情况下,您不希望使用所有槽都设置为默认值的创建对象。例如,如果要创建一个表示特定书籍的对象,您已经知道它的标题、作者和年份,并且您想使用这些值初始化一个新对象,而不是一些无用的空字符串。如果在槽定义中指定了 :initarg 选项,则可以在对象创建时使用用户指定的参数初始化此槽。可以通过向make-instance 提供相应的关键字参数来实现
(make-instance 'book :author "Paul Graham" :title "ANSI Common Lisp" :year 1995)
由于book 槽的默认值始终是无用的(没有年份 0,并且没有标题为空的书),因此应该删除 :initform 槽选项。然后,忘记初始化槽的用户将收到错误消息,并且将在下次提供所有必要信息以解决此问题。明智的做法是只为不打算更改的槽提供 :reader 函数(这是我们book 类中每个槽的情况)。简而言之,只提供那些可能对您有用的选项。