🎩 面向对象的艺术:Common Lisp中的结构和类


🌱 引言:模块化系统与面向对象编程

在编程的世界里,设计大规模系统的关键在于创造具有清晰接口和协议的模块化单元,而不是死板地局限于模块的具体实现方式。许多编程语言都依赖于 面向对象编程(OOP)来实现这种模块化的愿景。然而,随着时间推移,语言设计者逐渐意识到,单靠面向对象的模型并不足以应对复杂的编程需求。于是,模板(template)、命名空间(namespace)等概念应运而生。

而在这个过程中,许多语言却由于其静态特性和标准化的限制,无法灵活地扩展其对象模型。要想改进或增强这些模型,往往意味着要创造一门新的语言。痛苦!但对于 Common Lisp 来说,这种痛苦不存在。它一直以来都是一门 “可编程的语言”,并且在 CLOS(Common Lisp Object System) 标准化之前,Lisp 就有各种自定义的对象实现。

Common Lisp 的独特之处在于,它将面向对象编程的各个部分(如 继承封装多态数据抽象)拆解开来,使你可以根据具体问题的领域来灵活组合这些元素,而不是强行套用某种固定的解决方案。正如 Imagine Dragons 所唱的那样:“你让我崩溃,但又重塑了我,成为一个真正的信徒。”——Common Lisp 让我们摆脱了传统对象模型的束缚,给我们带来了新的信仰。

🏗️ 结构体的老路:defstruct 的基础

🧱 结构体的基本构成

早期的 Lisp 开发者是通过 抽象数据类型(ADT)结构体(struct) 来实现对象的概念。在 Common Lisp 中,这个概念通过 defstruct 实现。defstruct 是一种非常简单的方式,用于定义一个数据结构,它的主要优点之一就是 快速

例如,以下是一个简单的结构体定义:

(defstruct my-record
  "一个示例结构体"
  first-name
  last-name)

这个宏做了几件重要的事情:

  1. 定义了一个包含两个槽(slot)的聚合数据结构,分别是 first-namelast-name
  2. 提供了一个构造函数 make-my-record,可以通过关键词参数来初始化槽的值。例如:
(defvar an-instance nil)  ; 定义全局变量  
(setf an-instance (make-my-record :first-name "David" :last-name "Botton"))
  1. 提供了一个复制构造函数 copy-my-record,用于创建现有结构体实例的副本。
  2. 为每个槽自动生成了访问器函数。例如,(my-record-first-name an-instance) 返回 first-name 槽的值,而 (setf (my-record-first-name an-instance) "New Name") 则设置该槽的值。
  3. 将结构体加入类型层次中,例如 (typep an-instance 'my-record) 返回 T

⏳ 结构体的局限

虽然 defstruct 提供了一种轻量级的方式来定义数据结构,但它的功能相对有限。如果你需要更复杂的功能,例如多重继承、动态分发等,你可能更适合使用 defclassdefstruct 的一些优势(如速度)在现代编译器的优化下也变得不太重要了。

因此,虽然 defstruct 是 Lisp 历史中的一部分,但许多现代应用场景更倾向于使用 defclass 来实现更复杂的对象行为。

🏎️ 面向对象的进阶:defclass 的力量

⚙️ 类的基本构成

defclass 是 Common Lisp 中用于定义类的宏,它提供了更多的灵活性和功能。让我们从最简单的类定义开始:

(defclass my-record ()
  ((first-name
    :accessor first-name
    :initarg :first-name
    :initform "")
   (last-name
    :accessor last-name
    :initarg :last-name
    :initform "")))

这个类定义做了几件事:

  1. 定义了一个包含两个槽的聚合数据结构。
  2. 提供了一个构造函数 make-instance,允许通过关键词参数初始化槽的值。
  3. 没有生成复制构造函数,因为对象的复制可能涉及复杂的语义。
  4. 为每个槽生成了访问器函数,允许安全地访问和修改槽的值。
  5. 将类加入类型层次中,例如 (typep an-instance 'my-record) 返回 T

🧬 继承和多重继承

defclass 的一个强大功能是支持多重继承。我们可以创建一个继承自多个父类的子类:

(defclass phone-record ()
  ((phone
    :accessor phone)))

(defclass employee-record (my-record phone-record)
  ((title
    :accessor title)))

在这个例子中,employee-record 继承了 my-recordphone-record,因此它拥有四个槽:first-namelast-namephonetitle。通过这种方式,我们可以轻松地将多个类的功能组合到一个子类中。

🔧 方法与多态

在面向对象编程中,方法是与对象交互的主要方式。在 Common Lisp 中,方法通过 defmethod 定义。与其他语言不同的是,Common Lisp 提供了 多重分发 的能力,这意味着方法不仅可以根据一个对象的类型来选择执行的代码,还可以根据多个对象的类型进行选择。

例如,我们可以定义一个方法来设置员工的职位和电话号码:

(defmethod setup-new-employee ((employee employee-record) title phone)
  (setf (title employee) title)
  (setf (phone employee) phone))

然后我们可以像这样使用这个方法:

(setf an-instance (make-instance 'employee-record :first-name "George" :last-name "Forist"))
(setup-new-employee an-instance "Boss Man" "954-555-1212")

通过这种方式,我们可以根据不同的对象类型定义不同的行为,实现多态。

🎭 封装与泛型函数:defgeneric 的魔力

在 Common Lisp 中,封装通常通过 包(package) 来实现。你可以将类和方法封装在一个包中,限制外部代码对这些符号的访问,这样就实现了封装的概念。

此外,Common Lisp 还提供了 泛型函数(generic function),它允许你定义一个函数,但具体执行哪个方法则取决于传入的参数类型。例如:

(defgeneric setup-new-employee (employee-object title phone))

泛型函数通过动态分发的机制,确保在运行时根据对象的类型调用相应的方法。

🧑‍💻 结语:Lisp,面向对象的信徒

通过这篇文章,我们探索了 Common Lisp 中结构体和类的基本概念。虽然 defstruct 提供了一个简单快捷的方式来定义数据结构,但 defclass 及其相关的 defmethoddefgeneric 提供了更强大的功能,尤其是在多重继承和多态性方面。

Common Lisp 的灵活性和可扩展性使得它在面向对象编程领域独树一帜,正如 Imagine Dragons 的歌词里所说的那样:“你让我崩溃,但又重塑了我,成为一个真正的信徒。”


面向记忆的学习材料

快速学习并记住Common Lisp中结构体和类的相关知识,包括defstruct和CLOS (Common Lisp Object System)的基本概念和用法。

知识点: Common Lisp中的结构体(defstruct)
题目: 在Common Lisp中,defstruct宏的主要作用是什么?
选项:
A) 定义函数
B) 创建包
C) 定义抽象数据类型
D) 声明变量

正确答案: C
解析: defstruct宏用于定义抽象数据类型(ADT)或称为结构体。它创建一个聚合数据结构,包含多个槽(slots),并自动生成构造函数、复制函数和访问器。这是Common Lisp中实现面向对象编程概念的早期方式之一。
速记提示: 记住"struct"是"structure"的缩写,意味着"结构",因此defstruct就是定义一个结构化的数据类型。

知识点: defstruct生成的函数
题目: 以下哪个不是defstruct自动生成的函数?
选项:
A) 构造函数(make-struct-name)
B) 复制函数(copy-struct-name)
C) 访问器(struct-name-element)
D) 删除函数(delete-struct-name)

正确答案: D
解析: defstruct自动生成构造函数(make-struct-name)、复制函数(copy-struct-name)和每个成员的访问器(struct-name-element)。它不会生成删除函数,因为Lisp使用垃圾回收机制自动管理内存,不需要显式删除对象。
速记提示: 记住CCC - Create(构造), Copy(复制), Component access(成员访问)。

知识点: defstruct的封装性
题目: 在Common Lisp中,defstruct如何实现封装?
选项:
A) 通过内部方法
B) 通过私有变量
C) 通过包系统
D) defstruct本身不提供封装

正确答案: C
解析: defstruct本身不直接提供封装机制。在Common Lisp中,封装通常通过包系统(package system)实现。将defstruct定义放在单独的包中,可以控制哪些部分对外可见,从而实现封装。
速记提示: 想象将结构体"包装"在一个包里,实现封装。

知识点: Common Lisp对象系统(CLOS)
题目: CLOS代表什么?
选项:
A) Common Lisp Object Syntax
B) Common Lisp Object System
C) Common Lisp Oriented Structure
D) Common Lisp Object Standard

正确答案: B
解析: CLOS代表Common Lisp Object System,是Common Lisp的标准化面向对象编程系统。它提供了更强大和灵活的对象定义和方法调度机制,包括多重继承和多重方法分派。
速记提示: 记住CLOS中的O代表Object(对象),S代表System(系统),强调它是一个完整的对象系统。

知识点: defclass基本语法
题目: 在defclass定义中,以下哪个不是槽(slot)选项?
选项:
A) :accessor
B) :initarg
C) :initform
D) :inherit

正确答案: D
解析: defclass中常用的槽选项包括:accessor(定义访问器),:initarg(允许通过make-instance初始化),:initform(定义默认值)。:inherit不是有效的槽选项,继承是通过类定义的参数列表实现的。
速记提示: 记住AII - Accessor, Initarg, Initform是三个主要的槽选项。

知识点: defclass vs defstruct
题目: 相比defstruct,defclass的主要优势是什么?
选项:
A) 更快的执行速度
B) 自动生成复制构造函数
C) 支持多重继承
D) 更简单的语法

正确答案: C
解析: defclass相比defstruct的主要优势是支持多重继承。此外,它还提供了更细粒度的控制,如自定义构造函数和访问器,以及动态方法分派。虽然defstruct在某些情况下可能有轻微的性能优势,但defclass的功能更加强大和灵活。
速记提示: class比struct更"类"似真实世界的复杂关系,因此支持多重继承。

知识点: defmethod的作用
题目: defmethod的主要作用是什么?
选项:
A) 定义新的类
B) 创建泛型函数
C) 为特定类定义方法
D) 实现多重继承

正确答案: C
解析: defmethod用于为特定类或类的组合定义方法。它允许将函数与类直接关联,实现了面向对象编程中的多态性。defmethod通过指定参数的类型来限制方法的适用范围。
速记提示: method是方法,defmethod就是定义特定类的方法。

知识点: 泛型函数(Generic Functions)
题目: 在Common Lisp中,泛型函数的主要特点是什么?
选项:
A) 只能接受一个参数
B) 根据参数类型动态分派到适当的方法
C) 必须使用defgeneric显式定义
D) 不支持多重分派

正确答案: B
解析: 泛型函数的主要特点是能够根据参数的类型动态分派到适当的方法。这实现了运行时的多态性。泛型函数可以接受多个参数,支持多重分派,并且不一定要显式使用defgeneric定义(尽管显式定义有其好处)。
速记提示: 想象泛型函数如同一个智能路由器,根据"参数类型"这个地址,将调用分派到正确的方法。

知识点: defgeneric的用途
题目: 使用defgeneric显式定义泛型函数的主要好处是什么?
选项:
A) 提高程序执行速度
B) 自动生成文档
C) 定义清晰的接口和协议
D) 简化方法定义过程

正确答案: C
解析: 虽然不是必须的,但使用defgeneric显式定义泛型函数的主要好处是可以定义清晰的接口和协议。它帮助开发者明确指定函数的预期用法,包括参数数量和可能的类型。这对于创建大型系统和库特别有用,因为它提高了代码的可读性和可维护性。
速记提示: generic意味着"通用的",defgeneric就像是定义一个通用的接口或协议。

知识点: CLOS中的多重继承
题目: 在CLOS中,如何实现多重继承?
选项:
A) 使用多个defstruct定义
B) 在defclass的参数列表中列出多个父类
C) 使用special关键字
D) 通过defmethod实现

正确答案: B
解析: 在CLOS中,多重继承是通过在defclass的参数列表中列出多个父类来实现的。例如:(defclass employee-record (my-record phone-record) …)。这允许一个类继承多个父类的属性和方法,提供了强大的代码重用机制。
速记提示: 想象defclass的参数列表如同一个"家谱",列出了所有的"父辈"。

知识点: CLOS中的方法组合
题目: CLOS中的:after,:before,:around方法修饰符的作用是什么?
选项:
A) 定义方法的执行顺序
B) 指定方法的访问权限
C) 设置方法的优先级
D) 声明方法的返回类型

正确答案: A
解析: CLOS中的:after,:before,:around方法修饰符用于定义方法的执行顺序。:before方法在主方法之前执行,:after方法在主方法之后执行,:around方法可以包围主方法的执行。这种机制提供了强大的方法组合能力,允许在不修改原有方法的情况下扩展或修改行为。
速记提示: 想象这些修饰符如同舞台表演,:before是开场,:after是谢幕,:around是整场演出的框架。

知识点: CLOS中的槽选项
题目: 在CLOS中,:allocation槽选项的作用是什么?
选项:
A) 分配内存空间
B) 指定槽的存储位置
C) 设置槽的初始值
D) 定义槽的访问权限

正确答案: B
解析: 在CLOS中,:allocation槽选项用于指定槽的存储位置。它有两个主要值::instance(默认值,每个实例都有自己的槽)和:class(类槽,所有实例共享一个值)。这允许开发者控制数据是属于单个对象还是整个类。
速记提示: allocation关键词暗示了"分配"的概念,这里是分配存储位置。

知识点: CLOS中的多重分派
题目: CLOS支持多重分派的主要机制是什么?
选项:
A) 使用多个defclass定义
B) 通过defgeneric实现
C) 在defmethod中指定多个参数的类型
D) 使用特殊的dispatch关键字

正确答案: C
解析: CLOS支持多重分派的主要机制是在defmethod中指定多个参数的类型。这允许方法的选择不仅基于第一个参数(如传统的单分派OOP),而是基于多个参数的类型。这提供了更灵活的方法分派能力,特别适用于需要考虑多个对象交互的情况。
速记提示: 想象defmethod如同一个多维坐标系,每个参数类型都是一个维度,共同决定调用哪个方法。

知识点: CLOS中的实例初始化
题目: 在CLOS中,如何在创建对象时初始化槽值?
选项:
A) 使用defvar
B) 通过:initarg和make-instance
C) 只能在defclass中使用:initform
D) 必须在构造函数中手动设置

正确答案: B
解析: 在CLOS中,通过:initarg槽选项和make-instance函数来在创建对象时初始化槽值。在defclass中为槽定义:initarg,然后在调用make-instance时使用对应的关键字参数来设置初始值。例如:(make-instance 'my-class :slot-name value)。这提供了灵活的对象初始化机制。
速记提示: init在编程中通常表示"初始化",arg表示"参数",所以:initarg就是用于初始化的参数。

知识点: CLOS中的方法组合顺序
题目: 在CLOS中,标准方法组合的执行顺序是什么?
选项:
A) primary, :before, :after, :around
B) :before, primary, :after, :around
C) :around, :before, primary, :after
D) :before, :around, primary, :after

正确答案: C
解析: 在CLOS的标准方法组合中,执行顺序是::around方法(如果有),然后是所有:before方法,接着是最具体的primary方法,最后是所有:after方法。:around方法有能力完全控制其他方法的执行,包括是否调用其他方法。这种灵活的组合机制允许复杂的行为定制。
速记提示: 记住ABPA顺序 - Around, Before, Primary, After。想象:around如同舞台的帷幕,首先拉开,最后合上。

知识点: CLOS中的槽继承
题目: 在CLOS中,子类如何继承父类的槽?
选项:
A) 自动继承所有槽
B) 只继承使用:inherit选项的槽
C) 需要在子类中重新定义所有槽
D) 继承槽名,但可以重新定义槽的属性

正确答案: D
解析: 在CLOS中,子类自动继承父类的槽名,但可以重新定义继承的槽的属性。这意味着子类可以保留父类的槽结构,同时允许修改特定槽的特性,如访问器、初始值等。这种机制提供了继承的灵活性,允许子类根据需要定制继承的行为。
速记提示: 想象槽如同房间的布局,子类继承了房间的名字,但可以重新装修每个房间。

知识点: CLOS中的类优先级列表
题目: CLOS中的类优先级列表(Class Precedence List)的主要作用是什么?
选项:
A) 决定方法的执行顺序
B) 控制实例的创建顺序
C) 确定多重继承中的继承顺序
D) 管理类的内存分配

正确答案: C
解析: CLOS中的类优先级列表主要用于确定多重继承中的继承顺序。当一个类有多个父类时,类优先级列表决定了在方法查找、槽访问等操作中如何解析潜在的冲突。它保证了一致的行为,特别是在复杂的继承层次结构中。CLOS使用一种称为拓扑排序的算法来计算这个列表。
速记提示: 优先级列表就像是家族树的"重要性排序",决定在多个"父辈"中听谁的。

知识点: CLOS中的元对象协议(MOP)
题目: CLOS的元对象协议(MOP)主要提供什么功能?
选项:
A) 自动生成文档
B) 优化代码执行效率
C) 自定义和扩展CLOS本身
D) 简化类定义语法

正确答案: C
解析: CLOS的元对象协议(MOP)主要提供了自定义和扩展CLOS本身的能力。MOP允许程序员修改类、泛型函数、方法等的行为,甚至可以改变它们的实现方式。这使得CLOS成为一个高度可扩展的对象系统,能够适应各种特殊需求和编程范式。
速记提示: 元(Meta)意味着"关于自身的",所以元对象协议就是允许你改变对象系统本身的规则。

知识点: CLOS中的实例更新
题目: 在CLOS中,如何更新现有实例以适应类定义的变化?
选项:
A) 自动更新,无需操作
B) 使用update-instance-for-redefined-class函数
C) 必须创建新实例替换旧实例
D) 通过重新加载整个系统

正确答案: B
解析: 在CLOS中,当类定义发生变化时,可以使用update-instance-for-redefined-class函数来更新现有实例。这个函数允许开发者定义如何处理旧实例,包括如何初始化新增的槽、如何处理被移除的槽等。这提供了动态更新对象的强大机制,特别适用于长时间运行的系统。
速记提示: update暗示更新,redefined暗示重新定义,结合起来就是"为重新定义的类更新实例"。

知识点: CLOS中的方法组合类型
题目: 除了标准方法组合外,CLOS还内置支持哪些方法组合类型?
选项:
A) sum, max, min
B) and, or, progn
C) first, last, random
D) add, subtract, multiply

正确答案: B
解析: 除了标准方法组合,CLOS还内置支持and, or, progn等方法组合类型。这些组合类型改变了primary方法的组合方式:and组合在遇到nil返回值时停止并返回nil,or组合在遇到非nil值时停止并返回该值,progn组合执行所有方法并返回最后一个方法的值。这些组合类型提供了强大的方法组合灵活性。
速记提示: and, or, progn都是Lisp中常见的控制结构,CLOS将它们扩展到了方法组合中。

总结

本学习材料涵盖了Common Lisp中结构体(defstruct)和类(CLOS)的核心概念,包括:

  1. defstruct的基本用法和生成的函数
  2. CLOS的主要组成部分:defclass, defmethod, defgeneric
  3. 类的定义、继承和槽选项
  4. 方法的定义、组合和多重分派
  5. 泛型函数和动态分派机制
  6. CLOS的高级特性,如元对象协议(MOP)和实例更新

通过掌握这些概念,学习者应该能够理解Common Lisp中面向对象编程的基本原理和高级特性,为进一步学习和应用打下坚实基础。

参考文献

  1. Common Lisp - The Tutorial Part 11
  2. ANSI Common Lisp by Paul Graham
  3. Object-Oriented Programming in Common Lisp: A Programmer's Guide to CLOS by Sonya E. Keene

CLOS(Common Lisp Object System)中的:after:before:around方法修饰符用于定义方法的执行顺序,因此正确答案是:

A) 定义方法的执行顺序

详细解释:

在CLOS中,方法组合(Method Combination)允许你通过一些修饰符来控制在调用一个泛型函数时,多个方法的执行顺序和逻辑。这些修饰符主要有以下几种:

1. :before 方法修饰符

  • 作用:定义在主方法(primary method)之前执行的方法。
  • 特点:before方法会在主方法执行之前被自动调用,但它的返回值会被忽略,真正的返回值来自主方法或其他组合方法。

示例

(defmethod example :before ((obj my-class))
  (format t "Before method executed.



在调用example这个泛型函数时,定义的:before方法会在所有的主方法之前执行。

2. :after 方法修饰符

  • 作用:定义在主方法(primary method)之后执行的方法。
  • 特点:after方法会在主方法执行之后自动调用,且同样不会影响主方法的返回值。

示例

(defmethod example :after ((obj my-class))
  (format t "After method executed.



当调用example时,:after方法会在主方法执行之后调用。

3. :around 方法修饰符

  • 作用:定义一个包裹主方法执行的行为,通常用来执行一些前后逻辑或完全控制执行流程。
  • 特点:around方法有最高的优先级,它可以选择是否调用主方法。如果不显式调用call-next-method,主方法将不会被执行。

示例

(defmethod example :around ((obj my-class))
  (format t "Around method before.
  (call-next-method) ; 显式调用下一个方法
  (format t "Around method after.



在调用example时,这个:around方法会首先执行,且可以选择是否调用call-next-method来继续执行主方法或其他组合方法。

重点难点讲解:

  1. 执行顺序
  • :before方法:先执行,且不影响主方法的返回值。
  • 主方法:核心业务逻辑,通常返回最终结果。
  • :after方法:后执行,也不影响主方法的返回值。
  • :around方法:可以完全包裹主方法的执行,甚至可以选择不执行主方法。
  1. call-next-method
  • :around方法中,call-next-method的调用非常关键。如果不调用它,主方法和其他方法组合将不会被执行。
  1. 实际应用
  • 使用:before:after来实现一些前置和后置逻辑,如日志记录、资源初始化或清理。
  • 使用:around来实现更复杂的控制逻辑,如对方法调用进行拦截或条件执行。

总结:

CLOS中的:before:after:around方法修饰符主要用于定义方法的执行顺序,属于方法组合的一部分,允许开发者通过这些修饰符进行更灵活的控制。这也是CLOS强大之处之一,使得它可以支持复杂的面向对象编程模式。

因此,正确答案是 A) 定义方法的执行顺序


“主方法”指的是普通的、未带修饰符的方法,它在方法组合中通常作为核心的功能实现。与这些修饰符(:before:after:around)相比,主方法是最重要的,因为它通常包含了实际的业务逻辑。

接下来,我将通过一个完整的CLOS例子,展示如何使用主方法和这三个修饰符。

例子:银行账户的存款操作

我们通过一个简单的银行账户存款操作来说明主方法和不同修饰符的作用。

1. 定义一个简单的类 bank-account

(defclass bank-account ()
  ((balance :initform 0 :accessor balance)))

这里,bank-account类有一个属性balance,表示账户余额。我们使用balance来读取或更新账户余额。

2. 定义主方法 deposit

(defmethod deposit ((account bank-account) amount)
  (incf (balance account) amount)
  (format t "Deposited ~d. New balance: ~d



这是一个主方法,deposit方法接受两个参数:一个是bank-account对象,另一个是存款金额amount。它的作用是增加账户余额,并输出存款成功后的新余额。

3. 使用 :before 方法修饰符:

(defmethod deposit :before ((account bank-account) amount)
  (when (<= amount 0)
    (error "Deposit amount must be positive!")))

这里,我们使用:before修饰符实现一个前置检查,确保存款金额是正数。如果金额为0或负数,抛出一个错误。在主方法执行之前,这个:before方法会被调用。

4. 使用 :after 方法修饰符:

(defmethod deposit :after ((account bank-account) amount)
  (format t "Transaction completed successfully.



:after修饰符定义的方法会在主方法执行完毕后被调用。这里我们在存款成功后输出一条消息,表示交易完成。

5. 使用 :around 方法修饰符:

(defmethod deposit :around ((account bank-account) amount)
  (format t "Starting deposit process...
  (call-next-method)  ;; 执行主方法
  (format t "Deposit process finished.



:around修饰符定义的方法会包裹主方法的执行。在这个例子中,我们在存款操作开始和结束时分别输出一些调试信息。call-next-method用于调用主方法及其相关的:before:after方法。如果我们不调用call-next-method,主方法和其他组合方法将不会被执行。

完整代码:

(defclass bank-account ()
  ((balance :initform 0 :accessor balance)))

(defmethod deposit ((account bank-account) amount)
  (incf (balance account) amount)
  (format t "Deposited ~d. New balance: ~d

(defmethod deposit :before ((account bank-account) amount)
  (when (<= amount 0)
    (error "Deposit amount must be positive!")))

(defmethod deposit :after ((account bank-account) amount)
  (format t "Transaction completed successfully.

(defmethod deposit :around ((account bank-account) amount)
  (format t "Starting deposit process...
  (call-next-method)  ;; 执行主方法
  (format t "Deposit process finished.



执行效果:

假设我们创建一个银行账户并执行几次存款操作:

;; 创建账户对象
(setq my-account (make-instance 'bank-account))

;; 正常存款操作
(deposit my-account 100)

输出结果

Starting deposit process...
Deposited 100. New balance: 100
Transaction completed successfully.
Deposit process finished.

执行流程:

  1. :around方法首先被调用,用于输出“Starting deposit process…”。
  2. :before方法接着执行,检查存款金额是否为正数。
  3. 主方法执行,增加账户余额并输出存款成功后的信息。
  4. :after方法执行,输出“Transaction completed successfully.”。
  5. :around方法最后执行剩余部分,输出“Deposit process finished.”。

如果我们尝试存入一个负数:

(deposit my-account -50)

输出结果

Error: Deposit amount must be positive!

由于:before方法在主方法之前执行,它阻止了非法的存款操作。

详细说明:

1. :before的作用

  • 主要用于在主方法执行前做一些预处理或检查。它不会改变主方法的返回值,也不能阻止主方法的执行,除非显式抛出错误或其他异常。

2. :after的作用

  • 它在主方法执行后运行,通常用于一些后置操作,如日志记录或资源清理。无论主方法返回什么,:after方法的返回值都不会影响主方法的返回结果。

3. :around的作用

  • 它最灵活,可以包裹整个方法调用,并且有权决定是否调用call-next-method来继续执行主方法及其他组合方法。如果不调用call-next-method,主方法和其他组合方法就不会被执行。
  • 适合用来实现事务的开始或结束、条件性执行等复杂逻辑。

总结:

  • 主方法:是实现核心功能的普通方法。
  • :before方法:在主方法之前执行,适合做前置检查或初始化工作。
  • :after方法:在主方法之后执行,适合做后续处理,比如日志或清理工作。
  • :around方法:包裹整个方法调用,可以完全控制执行流程,甚至可以选择不调用主方法。

CLOS(Common Lisp Object System)支持多重分派的主要机制是通过在defmethod中指定多个参数的类型。这种方式允许方法根据多个参数的类型来确定哪一个具体的方法应该被调用,这正是多重分派的概念。

详细解释:

多重分派(Multiple Dispatch)意味着当调用一个通用函数(generic function)时,系统会根据多个参数的类型来选择合适的方法。在CLOS中,这通过方法组合(method combination)和基于参数类型的方法特化(method specialization)来实现。

CLOS的多重分派机制:

  1. defgeneric定义通用函数
    通用函数(generic function)是一个抽象的函数签名,可以有多个方法(defmethod)实现。每个方法可以为不同的参数类型进行特化。
  2. defmethod定义具体方法
    在定义方法时,可以给每个参数指定特定的类型,这样CLOS可以根据调用时传入的参数类型选择最合适的方法。
  3. 多重分派
    当调用一个通用函数时,CLOS会根据所有参数的类型,选择与这些参数匹配的最具体的实现。这就是多重分派的核心:不仅根据第一个参数类型,还根据其他参数的类型来分派到合适的方法

代码示例:

;; 定义两个类
(defclass animal () ())
(defclass dog (animal) ())

;; 定义一个通用函数 `interact`
(defgeneric interact (x y))

;; 根据参数的类型定义不同的实现
(defmethod interact ((x animal) (y animal))
  (format t "Animal interacts with another animal.

(defmethod interact ((x dog) (y animal))
  (format t "Dog interacts with an animal.

(defmethod interact ((x dog) (y dog))
  (format t "Dog interacts with another dog.

;; 调用不同的情况
(let ((rex (make-instance 'dog))
      (generic-animal (make-instance 'animal)))
  (interact rex generic-animal)  ;; 输出: Dog interacts with an animal.
  (interact rex rex))            ;; 输出: Dog interacts with another dog.

解析:

  • 我们定义了一个通用函数 interact,并为其定义了多种方法。
  • 通过在 defmethod 中为参数指定不同的类型,就实现了多重分派:系统会根据传入的参数类型来选择最合适的实现。
  • 当两个dog交互时,系统会调用 (dog dog) 版本的方法;当一个dog与通用animal交互时,会调用 (dog animal) 版本。

其他选项
解析:

  • A) 使用多个defclass定义
    定义多个类(defclass)用于创建不同的类型对象,但定义类本身并不是多重分派的机制。类的定义只是为多重分派的参数类型提供了基础。
  • B) 通过defgeneric实现
    defgeneric用于声明通用函数,但它本身并不实现多重分派。多重分派的实现体现在具体的defmethod中。
  • D) 使用特殊的dispatch关键字
    CLOS中并没有一个名为dispatch的特殊关键字用于实现多重分派。

总结:

CLOS中的多重分派主要通过在defmethod中为参数指定类型来实现。系统根据调用时参数的类型来选择合适的方法,因此选项 C 是正确的。


在CLOS(Common Lisp Object System)中,创建对象时初始化槽值的主要机制是通过:initargmake-instance的组合。这允许在使用make-instance创建对象时,传递初始化参数来设置对象的槽值。

详细解释:

CLOS中的实例初始化:

  1. defclass定义类和槽
    在CLOS中,类的定义使用defclass,其中一个类包含若干“槽”(slots)。槽是对象的属性,它们可以在创建对象时通过初始化参数进行赋值。
  2. :initarg指定初始化参数
    defclass中定义槽时,可以使用:initarg选项来指定一个关键字,这个关键字用于在创建对象时传递值给该槽。
  3. make-instance创建对象
    创建对象时使用的是make-instance函数。你可以通过make-instance传递与:initarg匹配的关键字参数来初始化槽。

示例代码:

;; 定义一个类 person,包含两个槽:name 和 age
(defclass person ()
  ((name :initarg :name    ;; 使用 :initarg :name 允许通过 :name 关键字初始化
         :initform "Unknown"  ;; 默认值为 "Unknown"
         :accessor person-name)  ;; 定义读取器方法
   (age :initarg :age        ;; 使用 :initarg :age 允许通过 :age 关键字初始化
        :initform 0          ;; 默认值为 0
        :accessor person-age)))  ;; 定义读取器方法

;; 使用 make-instance 创建对象,并通过 :initarg 传递初始值
(let ((p (make-instance 'person :name "Alice" :age 30)))
  (format t "Name: ~a, Age: ~a



解析:

  • 类定义
    我们定义了一个类person,其中包含两个槽nameage。每个槽都有一个:initarg,分别为:name:age,用于在创建对象时传递初始值。还定义了默认值(:initform),以及读取器方法(:accessor)。
  • 对象创建
    在创建person对象时,我们使用make-instance函数,并通过:name:age关键字传入槽的初始值。最终输出的结果是:
  Name: Alice, Age: 30

其他选项
解析:

  • A) 使用defvar
    defvar用于定义全局变量,并不用于对象槽值的初始化,因此这是不正确的。
  • C) 只能在defclass中使用:initform
    :initform确实可以为槽提供默认值,但它只能提供默认值,不能替代:initarg的功能。初始化时传入的值会优先于:initform,所以这个选项是错误的。
  • D) 必须在构造函数中手动设置
    CLOS不需要手动定义构造函数,make-instance已经内置了这种行为。因此,不需要手动设置槽值,可以通过:initargmake-instance自动完成对象的初始化。

总结:

在CLOS中,创建对象时初始化槽值的推荐方法是通过:initargmake-instance

0 0 投票数
Article Rating
订阅评论
提醒
0 评论
最旧
最新 最多投票
内联反馈
查看所有评论
0
希望看到您的想法,请您发表评论x