Common Lisp class 槽的访问函数

Accessor functions for Common Lisp class slots

我目前正在阅读 Peter Seibel 的 Practical Common Lisp 中关于 类 的章节,我对 accessor functions.

的使用感到困惑

设置

我不理解下面给出的涉及银行账户和客户名称的示例的 setf 函数的新定义:

(defun (setf customer-name) (name account)
  (setf (slot-value account 'customer-name) name))

用法如下:

(setf (customer-name my-account) "Sally Sue")

为什么 setf 的定义有两个参数 (name account),但我们提供的不是这个?上面的customer-name函数和后面定义的customer-namereader函数有什么关系吗(见下文)?

(defgeneric (setf customer-name) (value account))

(defmethod (setf customer-name) (value (account bank-account))
  (setf (slot-value account 'customer-name) value))

直接插槽访问

访问函数的动机是避免直接访问槽;接口优于实现等等。但是 Common Lisp 提供了 :reader:writer:accessor 插槽选项来做到这一点。例如

(customer-name
 :initarg :customer-name
 :initform (error "Must supply a customer name.")
 :accessor customer-name)

我的理解是否正确,这应该只用于绝对可以直接访问的插槽?因为如果我们稍后决定插槽 不应该 直接访问,我们会破坏事情。

setf有点神奇。 setf 的要点是使设置值的语法类似于访问该值的语法。对于使用 defun(或 defmethod)定义 setf 函数的情况,(setf (f ...) val) 等同于 (funcall #'(setf f) val ...)。这就是为什么 setf 在您的示例中只接受一个参数;传递给 (setf customer-name) 的第二个参数是 my-account。如果您想了解更多关于 setf 的内部信息,我写了一篇关于它的博客 post,您可以找到 here.

因为将读取器和写入器写入槽非常普遍,所以 defclass 提供了 :reader:writer:accessor 选项。当您传入这些选项之一时,defclass 将自动写入相应的读者和作者。例如槽定义:

(customer-name
  :accessor customer-name
  ...)

会自动写入代码:

(defgeneric customer-name (account))

(defmethod customer-name ((account bank-account))
  (slot-value account 'customer-name))

(defgeneric (setf customer-name) (value account))

(defmethod (setf customer-name) (value (account bank-account))
  (setf (slot-value account 'customer-name) value))

给你!

至于你关于直接访问插槽的问题,我看到的最常见的模式是为所有插槽制作访问器,然后只导出应该可以直接访问的插槽的访问器。这样,您可以直接从与 class 定义相同的包中访问所有插槽,但是从不同的包中您只能访问 "exported" 插槽。

函数作为 setter

(defun (setf customer-name) (name account)
  (setf (slot-value account 'customer-name) name))

(setf customer-name)其实就是函数名。是的,在 Common Lisp 中,函数名可以是这样的列表——而不仅仅是符号。参数是新 name 和客户 account。然后它在 account 实例中设置 customer-name 插槽。

(setf (customer-name my-account) "Sally Sue")

所以你使用访问器形式,在它周围写一个 setf 和新值。 Common Lisp 然后找出哪个 setter 属于访问器形式。这是您之前定义的函数。 my-account是对象,"Sally Sue"是新的customer-name

作为 setter

的泛型函​​数
(defgeneric (setf customer-name) (value account))

这定义了一个与上面类似的函数,但这次它是一个 CLOS 泛型函数 而不是由 defun。 Common Lisp 有普通函数和泛型函数。后者是 Common Lisp 对象系统 的一部分,并提供动态多重调度等功能。此通用函数将替换任何其他函数定义。

(defmethod (setf customer-name) (value (account bank-account))
  (setf (slot-value account 'customer-name) value))

这定义了一个方法,类似于上面的函数,但这次它是一个名为 (setf customer-name) 的通用函数的方法——记住这个列表是一个函数名。此方法是为帐户的 valuebank-account class 的所有 class 定义的。因此您只能用它来设置银行账户(及其子classes)的客户名称。

DEFCLASS 中定义的设置器

如您所述,DEFCLASS 可以通过提供适当的插槽选项来定义 setter。这只是一种方便,所以 class 的大部分定义都可以放在一个地方。

信息隐藏和其他软件工程原理

何时何地实际使用setter函数以及何时使用直接插槽访问只是风格问题和软件工程原理。 Common Lisp 没有任何限制。由您来定义保持软件可维护性的规则。您还可以遵循 object-oriented 软件设计的常用最佳实践。请注意,在 Common Lisp 中,更多的是风格和约定,因为 Common Lisp 没有真正的模块系统。它有符号包,但这不是真正的模块系统,因为它没有为 classes、插槽 and/or 通用函数定义可见性范围。