Skip to content

LISP - `:around` 方法组合

Common Lisp 对象系统 (CLOS) 提供了一个强大的特性,称为方法组合(method combination)。标准方法组合使用主方法(primary methods)以及 :before、:after 和 :around 辅助方法(auxiliary methods)。其中,:around 是最强大的,因为它允许你编写在主方法之前和之后运行的代码,从而有效地创建一个包装器。

可以把 :around 方法想象成 Web 框架中的中间件或 Python 中的装饰器。它可以检查参数、设置特殊上下文、运行核心逻辑,然后处理结果或执行清理工作。

:around 方法通过在 defmethod 中添加 :around 限定符来定义。其强大之处在于 call-next-method 函数。

;; 1. 定义一个泛型函数。
(defgeneric process-data (data))
;; 2. 为此泛型函数定义一个 :around 方法。
(defmethod process-data :around ((data t))
(format t "[Wrapper] Before processing data: ~a~%" data)
;; 3. 使用 `call-next-method` 调用下一个最特化的方法。
(let ((result (call-next-method)))
(format t "[Wrapper] After processing. Result was: ~a~%" result)
;; 4. :around 方法可以返回原始结果或修改后的结果。
result))

call-next-method 函数将控制权转移给下一个适用的方法(可以是另一个 :around 方法或主方法)。如果没有 call-next-method,主方法将不会被执行。

当 process-data 被调用时,流程如下:

  1. :around 方法首先被调用。
  2. call-next-method 之前的代码运行。
  3. call-next-method 调用适用的主方法(或其他辅助方法)。
  4. 主方法运行并返回一个值。
  5. 控制权返回到 :around 方法,返回值绑定到 result。
  6. call-next-method 之后的代码运行。
  7. :around 方法返回其最终值。

让我们添加针对不同数据类型特化的主方法。

;;; --- 泛型函数和 :around 方法(同上)---
(defgeneric process-data (data))
(defmethod process-data :around ((data t))
(format t "[Wrapper] Before processing data: ~a~%" data)
(let ((result (call-next-method)))
(format t "[Wrapper] After processing. Result was: ~a~%" result)
result))
;;; --- 主方法 ---
;; 定义一个针对整数特化的主方法。
(defmethod process-data ((data integer))
(format t " (Primary) Processing an integer...~%")
(* data 2))
;; 定义一个针对字符串特化的主方法。
(defmethod process-data ((data string))
(format t " (Primary) Processing a string...~%")
(string-upcase data))
;;; --- 调用 ---
(format t "--- Calling with an integer ---~%")
(process-data 10)
(format t "~%--- Calling with a string ---~%")
(process-data "hello world")
--- Calling with an integer ---
[Wrapper] Before processing data: 10
(Primary) Processing an integer...
[Wrapper] After processing. Result was: 20
--- Calling with a string ---
[Wrapper] Before processing data: hello world
(Primary) Processing a string...
[Wrapper] After processing. Result was: "HELLO WORLD"
  • 资源管理:确保文件或网络连接等资源被关闭(例如,在 :around 方法中使用 unwind-protect)。
  • 缓存/记忆化:在调用 call-next-method 之前检查结果是否已缓存。
  • 日志和追踪:在进入时打印参数,在退出时打印结果。
  • 参数验证:在将参数传递给主方法之前进行验证。
  • 条件执行:根据某些条件决定是否调用主方法。
  • 忘记 call-next-method:这是最常见的错误。如果你省略此调用,主方法将永远不会运行,你的泛型函数很可能不会达到你预期的效果。
  • 错误地修改参数:如果你想为下一个方法更改参数,你必须将它们传递给 call-next-method:(call-next-method new-arg1 new-arg2)。