Skip to content

LISP - 泛型函数

现代 Lisp:泛型函数和方法 (CLOS)

Section titled “现代 Lisp:泛型函数和方法 (CLOS)”

Common Lisp 对象系统(CLOS)提供了一种高度灵活且强大的面向对象编程范式。CLOS 的基石是泛型函数的概念,它允许你定义可以针对不同数据类型以不同方式实现的操作。

泛型函数使用 defgeneric 定义,它为一个操作建立名称和参数列表,但本身不包含任何实现逻辑。可以将其视为一个接口或协议,其他称为方法的专门函数将实现它。

在这里,我们定义一个接受一个参数的泛型函数 describe-entity。这个声明表明将有一个名为 describe-entity 的操作,但它的工作方式将取决于它接收的 entity 类型。

;; 定义我们函数的接口。
;; 它指定了名称 `describe-entity` 及其参数列表 `(entity)`。
(defgeneric describe-entity (entity))

方法使用 defmethod 定义,它为特定参数类型提供泛型函数的实际实现。在方法的参数列表中指定的类型称为特化器。当你调用泛型函数时,CLOS 会检查参数的类型并派发到最匹配的特定方法。

让我们定义一个简单的 player 类,然后为我们的 describe-entity 泛型函数创建两个方法。一个将处理 player 对象,另一个将处理简单的 string。

;; 首先,定义一个简单的类来使用。
(defclass player ()
((name :initarg :name
:accessor player-name
:documentation "玩家的姓名。")))
;; 定义一个专门用于 STRING 参数的方法。
(defmethod describe-entity ((entity string))
(format t "Entity is a simple string: ~s~%" entity))
;; 定义一个专门用于 PLAYER 对象的方法。
(defmethod describe-entity ((entity player))
(format t "Entity is a player named: ~a~%" (player-name entity)))
  • 派发:当你调用一个泛型函数,例如 (describe-entity some-object) 时,CLOS 执行方法派发。它会检查 some-object 的类,并找到最具体的匹配 defmethod。这就是动态派发,因为它发生在运行时。
  • 多重方法:Lisp 的派发不限于第一个参数。你可以针对多个参数进行特化,使其成为一个强大的多重方法系统。例如:(defmethod interact ((p1 player) (p2 monster)))。
  • 特化器:特化器是方法参数列表中指定类型的部分,例如 (entity string) 或 (entity player)。它告诉派发系统选择哪个方法。
  • 方法组合:CLOS 允许你以复杂的方式组合多个适用方法(例如,除了主方法之外,还可以运行 :before、:after 或 :around 方法)。这是构建高度可扩展系统的一个高级主题。

以下是一个完整、格式良好且可运行的示例,它将所有内容整合在一起。你可以将其保存为文件(例如 main.lisp),并使用 Common Lisp 实现(如 SBCL)运行它。

;; --- 类定义 ---
;; 使用 initargs 和 accessor 的现代类定义。
(defclass player ()
((name :initarg :name
:accessor player-name
:documentation "玩家的姓名。")))
;; --- 泛型函数定义 ---
;; 定义函数的签名。
(defgeneric describe-entity (entity))
;; --- 方法定义 ---
;; 专门用于 STRING 参数的方法。
(defmethod describe-entity ((entity string))
(format t "[LOG] Describing a string: ~s~%" entity))
;; 专门用于 PLAYER 对象的方法。
(defmethod describe-entity ((entity player))
(format t "[LOG] Describing a player named: ~a~%" (player-name entity)))
;; --- 主执行逻辑 ---
(defun main ()
;; 创建 PLAYER 类的实例。
(let ((hero (make-instance 'player :name "Aragorn"))
(npc-name "Bilbo"))
;; 使用 STRING 调用泛型函数。
;; CLOS 派发到字符串特化方法。
(describe-entity npc-name)
;; 使用 PLAYER 对象调用泛型函数。
;; CLOS 派发到玩家特化方法。
(describe-entity hero)))
;; 运行代码:
(main)
;; --- 常见错误与调试 ---
;; 如果使用没有方法的类型调用它会怎样?
;; (describe-entity 123)
;; 这将引发 `NO-APPLICABLE-METHOD` 错误,告诉你
;; Lisp 找不到针对 `FIXNUM`(一个整数)类型的方法。

当你加载并运行此文件时,输出将是:

[LOG] Describing a string: "Bilbo"
[LOG] Describing a player named: Aragorn
  • 可扩展系统:使用泛型函数可以允许你的库用户为他们自己的自定义类添加新行为,而无需修改你的源代码。
  • 游戏开发:一个 (draw object) 泛型函数可以有绘制不同游戏实体(如 player、enemy、item 等)的方法。
  • 数据处理:一个 (encode data) 泛型函数可以特化以处理不同数据格式(例如 JSON、XML、二进制)的编码。
  • 最佳实践:始终将 defclass 与 :initarg 用于构造函数参数,并与 :accessor(或 :reader/:writer)用于槽访问。这是在 Common Lisp 中定义类的标准、惯用方式。