您的对象只是列表,您将很难区分不同类型的形状。您可以在列表前添加关键字、标签类型(例如:point
、:circle
等),以根据该标签更好地调度您的移动操作,但这将是重新发明轮子,也就是对象。
简单的函数和列表
一种可以移动所有形状的功能
您可以这样做,前提是您可以分派您正在使用的实际对象类型。move
应该能够知道被移动的是什么样的形状。如果可以将对象类型添加为列表的 CAR,请更改数据结构,并使用 CASE 调度,然后根据需要移动每个对象。
或具有相同名称的多个函数。
这是不可能的,至少在同一个包中是不可能的。
克洛斯
(defpackage :pic (:use :cl))
(in-package :pic)
多个形状都有颜色,所以让我们定义一个类来表示具有颜色组件的对象:
(defclass has-color ()
((color :initarg :color :accessor color)))
如果你不熟悉CLOS(Common Lisp Object System),上面定义了一个名为 的类has-color
,没有超类和单个插槽,color
. 访问器命名读取器和写入器通用函数,以便您可以(color object)
检索对象并将(setf (color object) color)
对象的颜色设置为颜色。:initarg
用于定义要在 中使用的关键字参数make-instance
。
下面,我们定义 a point
,它具有颜色和附加值x
和y
坐标。
(defclass point (has-color)
((x :initarg :x :accessor x)
(y :initarg :y :accessor y)))
圆也是一样的:
(defclass circle (has-color)
((center :initarg :center :accessor center)
(radius :initarg :radius :accessor radius)))
还有一个多边形:
(defclass polygon (has-color)
((points :initarg :points :accessor points)))
最后,图片是一系列形状:
(defclass picture ()
((shapes :initarg :shapes :accessor shapes)))
您可以按如下方式制作一个圆圈:
(make-instance 'circle
:center (make-instance 'point :x 10 :y 30)
:color :black))
如果需要,您还可以定义更短的构造函数。
现在,您可以对move
对象使用通用函数。您首先使用 定义它DEFGENERIC
,它声明了泛型函数的签名以及其他选项。
(defgeneric move (object dx dy)
(:documentation "Move OBJECT by DX and DY"))
现在,您可以向该泛型函数添加方法,并且您的泛型函数将基于一个或多个专业化器和/或限定符分派给它们。
例如,您按如下方式移动一个点:
(defmethod move ((point point) dx dy)
(incf (x point) dx)
(incf (y point) dy))
可以看到我们move
是根据第一个参数的类进行专门化的,这里命名为point
。当绑定到的值point
是 class时应用该方法point
。INCF
隐式调用(setf x)
and的调用(setf y)
,如上定义。
移动一个圆意味着移动它的中心:
(defmethod move ((circle circle) dx dy)
(move (center circle) dx dy))
您可以在任何类上专门化一个泛型函数,例如标准SEQUENCE
类。它以相同的偏移量移动序列中的所有对象:
(defmethod move ((sequence sequence) dx dy)
(map () (lambda (object) (move object dx dy)) sequence))
这对多边形很有用:
(defmethod move ((polygon polygon) dx dy)
(move (points polygon) dx dy))
还有图片:
(defmethod move ((picture picture) dx dy)
(move (shapes picture) dx dy))
不可变版本
您也可以move
构建新实例,但这需要以某种方式复制现有对象。一种简单的方法是使用一个通用函数,该函数用源实例填充目标实例:
(defgeneric fill-copy (source target)
(:method-combination progn))
这里的方法组合是指所有满足的方法fill-copy
都运行,而不是只运行最具体的一个。这progn
表明所有方法都在一个progn
块中运行,一个接一个。通过上面的定义,我们可以定义一个简单的copy-object
泛型函数:
(defgeneric copy-object (source)
(:method (source)
(let ((copy (allocate-instance (class-of source))))
(fill-copy source copy)
copy)))
上面定义了一个名为 的通用函数copy-object
,以及 T 类型对象(任何对象)的默认方法。
ALLOCATE-INSTANCE
创建一个实例但不初始化它。该方法用于FILL-COPY
复制槽值。
例如,您可以定义如何复制color
具有颜色的任何对象的插槽:
(defmethod fill-copy progn ((source has-color) (target has-color))
(setf (color target) (color source)))
请注意,您在这里有多个分派:源对象和目标对象都必须属于has-color
要调用的方法的类。progn
方法组合允许fill-copy
在不同的、解耦的方法之间分配工作:
(defmethod fill-copy progn ((source point) (target point))
(setf (x target) (x source))
(setf (y target) (y source)))
如果您指出fill-copy
,则可以根据 的类层次结构应用两种方法point
:一种是为 定义的has-color
,另一种是专门针对point
该类的(用于两个参数)。progn
方法组合确保两者都被执行。
由于某些插槽可以未绑定,因此可能会fill-copy
失败。我们可以通过添加一个错误处理程序来解决 fill-copy
这个问题:
(defmethod fill-copy :around (source target)
(ignore-errors (call-next-method)))
该(call-next-method)
表单调用其他方法(由progn
限定符定义的方法),但我们将其包装在ignore-errors
. 这里没有定义颜色,但复制成功:
(copy-object (make-point :x 30 :y 20))
=> #<POINT {1008480D93}>
我们现在可以保留我们现有的、变异的move
方法,并将它们包装在一个:around
专门的方法中,首先制作一个副本:
(defmethod move :around (object dx dy)
;; copy and mutate
(let ((copy (copy-object object)))
(prog1 copy
(call-next-method copy dx dy))))
为了看看会发生什么,定义一个方法PRINT-OBJECT
:
(defmethod print-object ((point point) stream)
(print-unreadable-object (point stream :identity t :type t)
(format stream "x:~a y:~a" (x point) (y point))))
现在,移动一个点会创建一个新点:
(let ((point (make-instance 'point :x 10 :y 20)))
(list point (move point 10 20)))
=> (#<POINT x:10 y:20 {1003F7A4F3}> #<POINT x:20 y:40 {1003F7A573}>)
您仍然需要更改 SEQUENCE 类型的方法,该类型当前丢弃 的返回值move
,但除此之外,对现有代码几乎没有更改。
另请注意,上述方法主要用作描述 CLOS 各种用途的一种方式,实际上您可能会选择一种或另一种方式来移动点(可变或不可变),或者您将拥有不同的功能而不是单一的通用的一个(例如 mut-move 和 move)。