🌱 引言:模块化系统与面向对象编程
在编程的世界里,设计大规模系统的关键在于创造具有清晰接口和协议的模块化单元,而不是死板地局限于模块的具体实现方式。许多编程语言都依赖于 面向对象编程(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)
这个宏做了几件重要的事情:
- 定义了一个包含两个槽(slot)的聚合数据结构,分别是
first-name
和last-name
。 - 提供了一个构造函数
make-my-record
,可以通过关键词参数来初始化槽的值。例如:
(defvar an-instance nil) ; 定义全局变量
(setf an-instance (make-my-record :first-name "David" :last-name "Botton"))
- 提供了一个复制构造函数
copy-my-record
,用于创建现有结构体实例的副本。 - 为每个槽自动生成了访问器函数。例如,
(my-record-first-name an-instance)
返回first-name
槽的值,而(setf (my-record-first-name an-instance) "New Name")
则设置该槽的值。 - 将结构体加入类型层次中,例如
(typep an-instance 'my-record)
返回T
。
⏳ 结构体的局限
虽然 defstruct
提供了一种轻量级的方式来定义数据结构,但它的功能相对有限。如果你需要更复杂的功能,例如多重继承、动态分发等,你可能更适合使用 defclass
。defstruct
的一些优势(如速度)在现代编译器的优化下也变得不太重要了。
因此,虽然 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 "")))
这个类定义做了几件事:
- 定义了一个包含两个槽的聚合数据结构。
- 提供了一个构造函数
make-instance
,允许通过关键词参数初始化槽的值。 - 没有生成复制构造函数,因为对象的复制可能涉及复杂的语义。
- 为每个槽生成了访问器函数,允许安全地访问和修改槽的值。
- 将类加入类型层次中,例如
(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-record
和 phone-record
,因此它拥有四个槽:first-name
、last-name
、phone
和 title
。通过这种方式,我们可以轻松地将多个类的功能组合到一个子类中。
🔧 方法与多态
在面向对象编程中,方法是与对象交互的主要方式。在 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
及其相关的 defmethod
和 defgeneric
提供了更强大的功能,尤其是在多重继承和多态性方面。
Common Lisp 的灵活性和可扩展性使得它在面向对象编程领域独树一帜,正如 Imagine Dragons 的歌词里所说的那样:“你让我崩溃,但又重塑了我,成为一个真正的信徒。”
面向记忆的学习材料
快速学习并记住Common Lisp中结构体和类的相关知识,包括defstruct和CLOS (Common Lisp Object System)的基本概念和用法。
知识点: Common Lisp中的结构体(defstruct) 知识点: defstruct生成的函数 知识点: defstruct的封装性 知识点: Common Lisp对象系统(CLOS) 知识点: defclass基本语法 知识点: defclass vs defstruct 知识点: defmethod的作用 知识点: 泛型函数(Generic Functions) 知识点: defgeneric的用途 知识点: CLOS中的多重继承 知识点: CLOS中的方法组合 知识点: CLOS中的槽选项 知识点: CLOS中的多重分派 知识点: CLOS中的实例初始化 知识点: CLOS中的方法组合顺序 知识点: CLOS中的槽继承 知识点: CLOS中的类优先级列表 知识点: CLOS中的元对象协议(MOP) 知识点: CLOS中的实例更新 知识点: CLOS中的方法组合类型 本学习材料涵盖了Common Lisp中结构体(defstruct)和类(CLOS)的核心概念,包括: 通过掌握这些概念,学习者应该能够理解Common Lisp中面向对象编程的基本原理和高级特性,为进一步学习和应用打下坚实基础。 CLOS(Common Lisp Object System)中的 A) 定义方法的执行顺序 在CLOS中,方法组合(Method Combination)允许你通过一些修饰符来控制在调用一个泛型函数时,多个方法的执行顺序和逻辑。这些修饰符主要有以下几种: 示例: 在调用 示例: 当调用 示例: 在调用 CLOS中的 因此,正确答案是 A) 定义方法的执行顺序。 “主方法”指的是普通的、未带修饰符的方法,它在方法组合中通常作为核心的功能实现。与这些修饰符( 接下来,我将通过一个完整的CLOS例子,展示如何使用主方法和这三个修饰符。 我们通过一个简单的银行账户存款操作来说明主方法和不同修饰符的作用。 这里, 这是一个主方法, 这里,我们使用 假设我们创建一个银行账户并执行几次存款操作: 输出结果: 如果我们尝试存入一个负数: 输出结果: 由于 CLOS(Common Lisp Object System)支持多重分派的主要机制是通过在 多重分派(Multiple Dispatch)意味着当调用一个通用函数(generic function)时,系统会根据多个参数的类型来选择合适的方法。在CLOS中,这通过方法组合(method combination)和基于参数类型的方法特化(method specialization)来实现。 CLOS中的多重分派主要通过在 在CLOS(Common Lisp Object System)中,创建对象时初始化槽值的主要机制是通过 在CLOS中,创建对象时初始化槽值的推荐方法是通过
题目: 在Common Lisp中,defstruct宏的主要作用是什么?
选项:
A) 定义函数
B) 创建包
C) 定义抽象数据类型
D) 声明变量
题目: 以下哪个不是defstruct自动生成的函数?
选项:
A) 构造函数(make-struct-name)
B) 复制函数(copy-struct-name)
C) 访问器(struct-name-element)
D) 删除函数(delete-struct-name)
题目: 在Common Lisp中,defstruct如何实现封装?
选项:
A) 通过内部方法
B) 通过私有变量
C) 通过包系统
D) defstruct本身不提供封装
题目: CLOS代表什么?
选项:
A) Common Lisp Object Syntax
B) Common Lisp Object System
C) Common Lisp Oriented Structure
D) Common Lisp Object Standard
题目: 在defclass定义中,以下哪个不是槽(slot)选项?
选项:
A) :accessor
B) :initarg
C) :initform
D) :inherit
题目: 相比defstruct,defclass的主要优势是什么?
选项:
A) 更快的执行速度
B) 自动生成复制构造函数
C) 支持多重继承
D) 更简单的语法
题目: defmethod的主要作用是什么?
选项:
A) 定义新的类
B) 创建泛型函数
C) 为特定类定义方法
D) 实现多重继承
题目: 在Common Lisp中,泛型函数的主要特点是什么?
选项:
A) 只能接受一个参数
B) 根据参数类型动态分派到适当的方法
C) 必须使用defgeneric显式定义
D) 不支持多重分派
题目: 使用defgeneric显式定义泛型函数的主要好处是什么?
选项:
A) 提高程序执行速度
B) 自动生成文档
C) 定义清晰的接口和协议
D) 简化方法定义过程
题目: 在CLOS中,如何实现多重继承?
选项:
A) 使用多个defstruct定义
B) 在defclass的参数列表中列出多个父类
C) 使用special关键字
D) 通过defmethod实现
题目: CLOS中的:after,:before,:around方法修饰符的作用是什么?
选项:
A) 定义方法的执行顺序
B) 指定方法的访问权限
C) 设置方法的优先级
D) 声明方法的返回类型
题目: 在CLOS中,:allocation槽选项的作用是什么?
选项:
A) 分配内存空间
B) 指定槽的存储位置
C) 设置槽的初始值
D) 定义槽的访问权限
题目: CLOS支持多重分派的主要机制是什么?
选项:
A) 使用多个defclass定义
B) 通过defgeneric实现
C) 在defmethod中指定多个参数的类型
D) 使用特殊的dispatch关键字
题目: 在CLOS中,如何在创建对象时初始化槽值?
选项:
A) 使用defvar
B) 通过:initarg和make-instance
C) 只能在defclass中使用:initform
D) 必须在构造函数中手动设置
题目: 在CLOS中,标准方法组合的执行顺序是什么?
选项:
A) primary, :before, :after, :around
B) :before, primary, :after, :around
C) :around, :before, primary, :after
D) :before, :around, primary, :after
题目: 在CLOS中,子类如何继承父类的槽?
选项:
A) 自动继承所有槽
B) 只继承使用:inherit选项的槽
C) 需要在子类中重新定义所有槽
D) 继承槽名,但可以重新定义槽的属性
题目: CLOS中的类优先级列表(Class Precedence List)的主要作用是什么?
选项:
A) 决定方法的执行顺序
B) 控制实例的创建顺序
C) 确定多重继承中的继承顺序
D) 管理类的内存分配
题目: CLOS的元对象协议(MOP)主要提供什么功能?
选项:
A) 自动生成文档
B) 优化代码执行效率
C) 自定义和扩展CLOS本身
D) 简化类定义语法
题目: 在CLOS中,如何更新现有实例以适应类定义的变化?
选项:
A) 自动更新,无需操作
B) 使用update-instance-for-redefined-class函数
C) 必须创建新实例替换旧实例
D) 通过重新加载整个系统
题目: 除了标准方法组合外,CLOS还内置支持哪些方法组合类型?
选项:
A) sum, max, min
B) and, or, progn
C) first, last, random
D) add, subtract, multiply总结
参考文献
:after
、:before
、:around
方法修饰符用于定义方法的执行顺序,因此正确答案是:详细解释:
1.
:before
方法修饰符
:before
方法会在主方法执行之前被自动调用,但它的返回值会被忽略,真正的返回值来自主方法或其他组合方法。(defmethod example :before ((obj my-class))
(format t "Before method executed.
example
这个泛型函数时,定义的:before
方法会在所有的主方法之前执行。2.
:after
方法修饰符
: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
来继续执行主方法或其他组合方法。重点难点讲解:
:before
方法:先执行,且不影响主方法的返回值。:after
方法:后执行,也不影响主方法的返回值。:around
方法:可以完全包裹主方法的执行,甚至可以选择不执行主方法。
call-next-method
:
:around
方法中,call-next-method
的调用非常关键。如果不调用它,主方法和其他方法组合将不会被执行。
:before
和:after
来实现一些前置和后置逻辑,如日志记录、资源初始化或清理。:around
来实现更复杂的控制逻辑,如对方法调用进行拦截或条件执行。总结:
:before
、:after
、:around
方法修饰符主要用于定义方法的执行顺序,属于方法组合的一部分,允许开发者通过这些修饰符进行更灵活的控制。这也是CLOS强大之处之一,使得它可以支持复杂的面向对象编程模式。
:before
、:after
、:around
)相比,主方法是最重要的,因为它通常包含了实际的业务逻辑。例子:银行账户的存款操作
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.
执行流程:
:around
方法首先被调用,用于输出“Starting deposit process…”。:before
方法接着执行,检查存款金额是否为正数。:after
方法执行,输出“Transaction completed successfully.”。: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
方法:包裹整个方法调用,可以完全控制执行流程,甚至可以选择不调用主方法。
defmethod
中指定多个参数的类型。这种方式允许方法根据多个参数的类型来确定哪一个具体的方法应该被调用,这正是多重分派的概念。详细解释:
CLOS的多重分派机制:
defgeneric
定义通用函数:
通用函数(generic function)是一个抽象的函数签名,可以有多个方法(defmethod
)实现。每个方法可以为不同的参数类型进行特化。defmethod
定义具体方法:
在定义方法时,可以给每个参数指定特定的类型,这样CLOS可以根据调用时传入的参数类型选择最合适的方法。
当调用一个通用函数时,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)
版本。其他选项
defclass
定义:
定义多个类(defclass
)用于创建不同的类型对象,但定义类本身并不是多重分派的机制。类的定义只是为多重分派的参数类型提供了基础。defgeneric
实现:defgeneric
用于声明通用函数,但它本身并不实现多重分派。多重分派的实现体现在具体的defmethod
中。dispatch
关键字:
CLOS中并没有一个名为dispatch
的特殊关键字用于实现多重分派。总结:
defmethod
中为参数指定类型来实现。系统根据调用时参数的类型来选择合适的方法,因此选项 C 是正确的。
:initarg
与make-instance
的组合。这允许在使用make-instance
创建对象时,传递初始化参数来设置对象的槽值。详细解释:
CLOS中的实例初始化:
defclass
定义类和槽:
在CLOS中,类的定义使用defclass
,其中一个类包含若干“槽”(slots)。槽是对象的属性,它们可以在创建对象时通过初始化参数进行赋值。:initarg
指定初始化参数:
在defclass
中定义槽时,可以使用:initarg
选项来指定一个关键字,这个关键字用于在创建对象时传递值给该槽。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
,其中包含两个槽name
和age
。每个槽都有一个:initarg
,分别为:name
和:age
,用于在创建对象时传递初始值。还定义了默认值(:initform
),以及读取器方法(:accessor
)。
在创建person
对象时,我们使用make-instance
函数,并通过:name
和:age
关键字传入槽的初始值。最终输出的结果是: Name: Alice, Age: 30
其他选项
defvar
:defvar
用于定义全局变量,并不用于对象槽值的初始化,因此这是不正确的。defclass
中使用:initform
::initform
确实可以为槽提供默认值,但它只能提供默认值,不能替代:initarg
的功能。初始化时传入的值会优先于:initform
,所以这个选项是错误的。
CLOS不需要手动定义构造函数,make-instance
已经内置了这种行为。因此,不需要手动设置槽值,可以通过:initarg
和make-instance
自动完成对象的初始化。总结:
:initarg
和make-instance
。