跳转到内容

通用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 类中每个槽的情况)。简而言之,只提供那些可能对您有用的选项。

华夏公益教科书