Skip to content

LISP - 槽位与访问器

欢迎来到 Common Lisp 对象系统 (CLOS),这是现有的最强大和灵活的对象系统之一。在本章中,我们将学习基础知识:定义类、容纳其数据的槽位以及用于与数据交互的访问器。

类是创建对象的蓝图。我们使用 defclass 宏定义一个类。在其中,我们定义槽位,它们是类的数据成员。让我们为电商应用程序建模一个 product。

(defclass product ()
((id :initarg :id
:reader product-id)
(name :initarg :name
:accessor product-name)
(price :initarg :price
:accessor product-price
:initform 0.0
:type 'float)
(inventory-count :initarg :inventory
:accessor product-inventory
:initform 0
:type 'integer)))

每个槽位都通过一个名称和控制其行为的选项来定义:

  • :initarg:创建一个关键字(例如 :id),用于在使用 make-instance 创建对象时初始化此槽位。
  • :accessor:创建一个泛型函数(例如 product-name),可以使用 (setf (product-name an-instance) new-value) 读取槽位的值并写入槽位。
  • :reader:创建一个只能读取槽位值的函数(例如 product-id)。这对于像 ID 这样的不可变字段很有用。
  • :writer:(上面未用到)创建一个只能写入槽位的函数。它不如 :accessor 或 :reader 常见。
  • :initform:如果初始化时未提供槽位的值,则为其提供默认值(例如,price 默认为 0.0)。
  • :type:指定槽位预期的数据类型。这有助于错误检查和潜在的编译器优化。

我们使用 make-instance 创建一个类的实例。然后,我们使用定义的访问器和读取器函数来与其数据交互。

;; 如上所示定义 'product' 类
;; --- 创建类的实例 ---
(defvar *keyboard*
(make-instance 'product
:id "KBD-42"
:name "Mechanical Keyboard"
:price 99.99
:inventory 50))
;; --- 使用访问器和读取器读取数据 ---
(format t "--- Initial Product State ---~%")
(format t "ID: ~a (read-only)~%" (product-id *keyboard*))
(format t "Name: ~a~%" (product-name *keyboard*))
(format t "Price: $~,2f~%" (product-price *keyboard*))
(format t "In Stock: ~d~%~%" (product-inventory *keyboard*))
;; --- 使用访问器更新数据 ---
(format t "--- Updating Product State ---~%")
(format t "Putting the keyboard on sale...~%")
(setf (product-price *keyboard*) 79.99)
;; 顾客购买了一个
(decf (product-inventory *keyboard*))
;; --- 显示更新后的状态 ---
(format t "New Price: $~,2f~%" (product-price *keyboard*))
(format t "New Stock: ~d~%" (product-inventory *keyboard*))
;; --- 尝试写入只读槽位将导致错误 ---
; (setf (product-id *keyboard*) "NEW-ID") ; 这将引发错误

执行代码后,将返回以下结果:

--- Initial Product State ---
ID: KBD-42 (read-only)
Name: Mechanical Keyboard
Price: $99.99
In Stock: 50
--- Updating Product State ---
Putting the keyboard on sale...
New Price: $79.99
New Stock: 49
  • 封装:对不可变数据使用 :reader,对可变数据使用 :accessor 是封装的关键部分,可以保护对象免受无效状态更改。
  • make-instance:这是所有 CLOS 对象的通用构造函数。关键字参数的使用使得初始化代码可读且自文档化。
  • CLOS 的强大之处:这仅仅是开始。CLOS 的真正强大之处来自于泛型函数和 defmethod,它们允许你为不同的类定义专门的行为,从而实现真正的多态性。例如,你可以定义一个通用的 ship-item 函数,使其对 physical-product 和 digital-download 表现不同。