3

有一个通用的方法,比如说incx。有两个版本incx。一种专门针对类型a,一种专门针对类型b。类型b是 的子类a。给您一个类型的对象b,即派生类型,但您想调用专门针对类型的方法a。如果还没有专门针对 type 的同名方法,您可以轻松地做到这一点b,但是唉,有这样的方法。

a那么在这种情况下如何调用专门针对类型的方法呢?

(defclass a () ((x :accessor x :initform 0)))
(defclass b (a) ((y :accessor y :initform 0)))

(defgeneric inc (i))

(defmethod inc ((i a)) (incf (x i)))
(defmethod inc ((i b)) (incf (y i)))

(defvar r (make-instance 'b))

正如 CLOS 所承诺的,这调用了最专业的方法:

* (inc r) 
* (describe r)
    ..
  Slots with :INSTANCE allocation:
    X  = 0
    Y  = 1

但在这种特殊情况下,(不是一般情况下)我想要的是访问不太专业的版本。像这样说:

(inc (r a)) ; crashes and burns of course, no function r or variable a
(inc a::r)  ; of course there is no such scoping operator in CL

我看到call-next-method可以在专门的方法中使用该函数来获得下一个不那么专门的方法,但这不是这里想要的。

在被剪掉的代码中,我确实需要类似于 的东西call-next-method,但用于调用补充方法。与其在下一个不太专业化的类中调用同名方法,不如调用它的互补方法,该方法具有不同的名称。补充方法也是专门的,但调用这个专门的版本不起作用 - 原因call-next-method可能与包含的原因大致相同。专用于超类的所需方法并不总是具有相同的名称。

(call-next-method my-complement)  ; doesn't work, thinks my-complement is an arg

这是另一个例子

有一个描述电子特性的基类和一个描述“奇怪电子”特性的派生类。专门研究奇怪电子的方法希望调用专门研究电子的方法。为什么?因为这些方法为程序完成了正常电子部分的工作。奇怪电子的非电子部分几乎是微不足道的,或者更确切地说,如果它没有复制电子代码:

(defgeneric apply-velocity (particle velocity))
(defgeneric flip-spin (particle))

;;;; SIMPLE ELECTRONS

(defclass electron ()
  ((mass
      :initform 9.11e-31
      :accessor mass)
   (spin
      :initform -1
      :accessor spin)))

(defmacro sq (x) `(* ,x ,x))

(defmethod apply-velocity ((particle electron) v)
  ;; stands in for a long formula/program we don't want to type again:
  (setf (mass particle) 
        (* (mass particle) (sqrt (- 1 (sq (/ v 3e8)))))))

(defmethod flip-spin ((particle electron))
  (setf (spin particle) (- (spin particle))))

;;;; STRANGE ELECTRONS

(defclass strange-electron (electron)
  ((hidden-state
      :initform 1
      :accessor hidden-state)))

(defmethod flip-spin ((particle strange-electron))
  (cond
    ((= (hidden-state particle) 1)
     (call-next-method)

     ;; CALL ELECTRON'S APPLY-VELOCITY HERE to update
     ;; the electron. But how???
     )
    (t nil)))

;; changing the velocity of strange electrons has linear affect!
;; it also flips the spin without reguard to the hidden state!
(defmethod apply-velocity ((particle strange-electron) v)
  (setf (mass particle) (* (/ 8 10) (mass particle)))

  ;; CALL ELECTRON'S SPIN FLIP HERE - must be good performance,
  ;; as this occurs in critical loop code, i.e compiler needs to remove
  ;; fluff, not search inheritance lists at run time
  )

这一切都归结为一个简单的问题:

如果定义了更专业的方法,如何调用不太专业的方法?

4

5 回答 5

16

我更喜欢这里的显式方法:

(defun actually-inc-a (value) (incf (x value)))
(defun actually-inc-b (value) (incf (y value)))

(defmethod inc ((object a)) (actually-inc-a object))
(defmethod inc ((object b)) (actually-inc-b object))

即,将您要共享的实现部分放入单独的函数中。

(defun apply-velocity-for-simple-electron (particle v)
  (setf (mass particle) (* (mass particle) (sqrt (- 1 (sq (/ v 3e8)))))))

(defun flip-spin-for-simple-electron (particle)
  (setf (spin particle) (- (spin particle))))

(defmethod apply-velocity ((particle electron) v)
  (apply-velocity-for-simple-electron particle v))

(defmethod flip-spin ((particle electron))
  (flip-spin-for-simple-electron particle))

(defmethod apply-velocity ((particle strange-electron) v)
  (setf (mass particle) (* (/ 8 10) (mass particle)))
  (flip-spin-for-simple-electron particle))

(defmethod flip-spin ((particle strange-electron))
  (when (= (hidden-state particle) 1)
    (call-next-method)
    (apply-velocity-for-simple-electron particle #| Hu? What's the V here? |#)))

鉴于我对电子一无所知,无论是普通的还是奇怪的,旋转与否,我真的想不出一个有意义的名字来命名这些基本辅助函数。但除此之外...

于 2016-02-03T12:26:38.393 回答
16

您的问题包含两个问题:

  1. 如何调用具体有效的方法?
  2. 在电子模拟的情况下如何避免复制粘贴?

这个答案是我其他答案的合并,部分灵感来自德克对具体例子的好答案。我将首先介绍所提出的问题(调用特定方法)并解释为什么您应该尝试另一种方法,特别是对于您的示例。

调用有效的方法

是的,您可以调用与方法关联的函数,而不是通用函数。对于便携式方法,首先加载close-mop

(ql:quickload :closer-mop)

定义一些类和一个简单的泛型函数:

(defclass a () ())
(defclass b (a) ())
(defclass c (b) ())
(defgeneric foo (x)
  (:method ((x a)) 0)
  (:method ((x b)) (+ (call-next-method) 1))
  (:method ((x c)) (* (call-next-method) 2)))

我们有一个类层次结构(a < b < c)和一个仅在第一个参数上分派的通用函数。

现在,我们计算类b的适用方法,并使用结果列表来定义一个函数,该函数调用foospecial on b的有效方法。

(destructuring-bind (method . next)
    (closer-mop:compute-applicable-methods-using-classes
     #'foo
     (list (find-class 'b)))
  (let ((fn (closer-mop:method-function method)))
    (defun %foo-as-b (&rest args)
      (funcall fn args next))))

在这里你有两种不同的行为:

(let ((object (make-instance 'c)))
  (list
    (%foo-as-b object)
    (foo object))

=> (1 2)

但是不推荐这样做。CLOS 提供了一种组合有效方法的方法,您应该尝试按预期使用它而不是劫持它。事实上,假设我评估以下内容:

(defmethod foo :before ((i a)) (print "Before A"))

在c的实例cfoo调用的通用函数将打印字符串。但是当在c上使用时,不会打印任何字符串,即使我们调用函数时好像cb的实例并且该方法专门用于a%foo-as-b

这当然是因为compute-applicable-methods-using-classes取决于调用时已知的一组方法。在这种情况下,函数%foo-as-b仍在使用过时的方法列表。如果您定义几个这样的函数或专门针对多个类,效果会被放大。如果您想始终%foo-as-b与您的环境保持同步,您需要在每次调用此函数时重新计算列表(而不是使用 let-over-lambda,而是重新计算 lambda 内的值)。另一种可能性是在 CLOS 中引入钩子以在需要时重新计算函数,但这很疯狂。

不要过度使用继承来共享代码

考虑Liskov 替换原则。过度使用继承来共享代码(即实现细节)而不是多态是引发诸如“优先组合优于继承”之类的建议的原因。请参阅 “偏好组合优于继承”的概念从何而来?Code Smell: Inheritance Abuse了解更多详情。

使用函数

在 C++ 中,在哪里base::method()可以找到,你只是调用了一个具有相似名称的不同函数:当你告诉编译器你想调用哪个方法时,没有动态调度,所以这实际上就像你调用了一个常规函数.

根据您的要求,我会写以下内容。它基于 Dirk 的版本并利用辅助的内联局部函数,当您想避免重复时,这些函数非常合适:

(defclass electron ()
  ((mass :initform 9.11e-31 :accessor mass)
   (spin :initform -1 :accessor spin)))

(defclass strange-electron (electron)
  ((hidden-state :initform 1 :accessor hidden-state)))

(let ((light-speed 3e8)
      (mysterious-velocity 0d0))
  (flet ((%flip (p)
           (setf (spin p) (- (spin p))))
         (%velocity (p v)
           (setf (mass p)
                 (* (mass p)
                    (sqrt
                     (- 1 (expt (/ v light-speed) 2)))))))
    (declare (inline %flip %velocity))
    
    (defgeneric flip-spin (particle)
      (:method ((p electron))
        (%flip p))
      (:method ((p strange-electron))
        (when (= (hidden-state p) 1)
          (call-next-method)
          (%velocity p mysterious-velocity))))

    (defgeneric apply-velocity (particle velocity)
      (:method ((p electron) v)
        (%velocity p v))
      (:method ((p strange-electron) v)
        (setf (mass p)
              (* (/ 8 10) (mass p)))
        (%flip p)))))

问题已经解决,并且希望可读性强:无需在 CLOS 中破解其他内容。由不同方法共享的辅助函数很容易识别,如果您需要重新编译其中任何一个,则必须重新编译整个表单,这样可以确保在所有方法中都考虑到类之间现有的耦合。

使用组合

如果我们应用上述建议并改用组合会发生什么?让我们更改您的strange-electron,使其包含一个simple-electron. 就实际电子而言,这听起来可能很奇怪,但如果我们考虑用于模拟的物体,那就有意义了。另外,请注意,在您的问题中,您实际上写了“电子部分”“奇怪电子的非电子部分”。一、主要类:

;; Common base class
(defclass electron () ())

;; Actual data for mass and spin
(defclass simple-electron (electron)
  ((mass :initform 9.11e-31 :accessor mass)
   (spin :initform -1 :accessor spin)))

;; A strange electron with a hidden state
(defclass strange-electron (electron)
  ((simple-electron :accessor simple-electron :initarg :electron)
   (hidden-state :initform 1 :accessor hidden-state)))

请注意strange-electron不再继承自simple-electron(我们不需要存储单独的质量和自旋),而是包含simple-electron. 另请注意,我们添加了一个公共electron基类,在这种情况下这不是绝对必要的。我将跳过定义泛型函数的部分,只描述方法。为了获取/设置这些奇怪电子的质量和自旋,我们必须委托给内部对象:

(macrolet ((delegate (fn &rest args)
             `(defmethod ,fn (,@args (e strange-electron))
                (funcall #',fn ,@args (simple-electron e)))))
  (delegate mass)
  (delegate spin)
  (delegate (setf mass) new-value)
  (delegate (setf spin) new-value))

在我们继续之前,上面的代码做了什么?如果我们扩展 中的最后一种形式macrolet,即带有的形式(setf spin),我们将获得一个设置内部粒子槽的方法:

(defmethod (setf spin) (new-value (e strange-electron))
  (funcall #'(setf spin) new-value (simple-electron e)))

那太棒了。flip-spin现在,我们可以apply-velocity很简单地定义。基本行为与simple-electron类相关:

(defmethod flip-spin ((e simple-electron))
  (setf (spin e) (- (spin e))))

(defmethod apply-velocity ((e simple-electron) velocity)
  (setf (mass e)
        (* (mass e)
           (sqrt
            (- 1 (expt (/ velocity +light-speed+) 2))))))

这与您的原始问题中的等式相同,但专门针对simple-electron. 对于奇怪的电子,你依赖于内部对象:

(defmethod flip-spin ((e strange-electron))
  (when (= (hidden-state e) 1)
    (flip-spin (simple-electron e))
    (apply-velocity (simple-electron e) 0d0)))

(defmethod apply-velocity ((e strange-electron) velocity)
  (setf (mass e) (* (/ 8 10) (mass e)))
  (flip-spin (simple-electron e)))

您的目标之一是拥有一个 CLOS 接口而不是“静态接口”,这正是这里的情况。

结论

显式调用不太具体的方法是一种代码异味。我不排除在某些情况下它可能是一种明智的方法的可能性,但我建议首先考虑替代设计。

公共代码可以通过常规函数共享,就像它总是做的一样(为了方便定义always)。或者,更喜欢 composition

于 2016-02-03T08:52:09.800 回答
3

可以使用 MOP(MetaObect 协议)。似乎compute-applicable-methods可能正是您想要的。

也有可能使用change-class.

请注意,CLOS 中的方法不是“类方法”,它们是“泛型函数方法”。所以不能真正调用“父类中不同名称的方法”,只能调用不同的泛型函数。

于 2016-02-03T08:51:13.207 回答
3

PS:我知道这个答案很晚,但我仍然发现它是其他答案中尚未考虑的强大选择。


注意:对于专用于单个参数的方法,可以说下一个方法是专用于为专用参数提供的参数的超类的方法。

但是,这通常不成立例如,一个方法专门处理一个参数而另一个方法专门处理另一个参数,或者方法专门处理多个参数。


尽管如此,对于您手头的实际问题,您可以使用另一种方法,即使用一个特殊的变量来告诉您自己的方法简单call-next-method

(defvar *strange-electron-bypass* nil)

(defmethod flip-spin ((particle strange-electron))
  (let ((bypass *strange-electron-bypass*)
        (*strange-electron-bypass* nil))
    (cond (bypass
           (call-next-method))
          ((= (hidden-state particle) 1)
           (call-next-method)
           (let ((*strange-electron-bypass* t))
             ;; where does v come from?
             (apply-velocity particle v)))
          (t
           nil))))

(defmethod apply-velocity ((particle strange-electron) v)
  (let ((bypass *strange-electron-bypass*)
        (*strange-electron-bypass* nil))
    (cond (bypass
           (call-next-method))
          (t
           (setf (mass particle)
                 (* (/ 8 10) (mass particle)))
           (let ((*strange-electron-bypass* t))
             (flip-spin particle))))))

如果你只专注于类,调用flip-spin (strange-electron)inside的性能apply-velocity (strange-elector t)不会受到太大影响。在大多数(如果不是全部)CLOS 实现中,在这种情况下,将根据参数的类对适用的方法进行记忆(缓存),因此只有对strange-electron自身实例的第一次调用才会为计算适用的方法付出代价。

这种方法的优点是它是可泛化的,因为它会调用下一个最具体的方法,并且不需要弄乱 CLOS,这通常意味着失去由 Common Lisp 实现执行的优化。

编辑:如您所见,该变量在方法条目上*strange-electron-bypass*被反弹nil以支持递归、相互或其他方式。在这种情况下,没有递归,但是如果您想将此解决方案推广到可能存在递归的其他情况(即相同的方法在调用堆栈中适用两次),特别是在组合情况下,这些方法将是可重入的。

于 2016-02-10T11:21:27.293 回答
1

Dirk 的回答有几个可以解决的问题,如此处所示。

首先,它不会泛化而不成为一个新的静态对象系统。在尝试泛化时,很快就会遇到这样一个事实,即属于同一个泛型定义的所有方法都具有相同的名称。为了解决这个问题,剩下的就是给函数命名以反映它们的类型签名(根据 Stroustrup 著名的宏处理器)。

其次,当泛化它成为一个独立的静态面向对象系统。作为一个静态系统,它不能很好地与 CLOS 配合使用。它变成了一个混合范式的例子。

然而,避免代码重复的 Dirks 方法可以保留在本地,而无需将辅助例程导出到接口。这可以通过将它们包装在 CLOS 方法中来完成。这些 CLOS 方法然后成为特化树中的分支,可以与其他分支分开特化。然后名称更改代表一个分支而不是类型签名(更易于管理)。

所以这里是应用于 inc 示例的封装辅助函数方法。请注意,inc-a 变成了一个可以被其他人调用的不太特殊的函数,包括在继承的 b 类上专门化的方法,因为 b 类中没有方法进一步专门化它(与 inc 不同)。

(defclass a () ((x :accessor x :initform 0)))
(defclass b (a) ((y :accessor y :initform 0)))

(defgeneric inc (i))
(defgeneric inc-a (i)) ; same as inc, but won't be further specialized

(defmacro inc-a-stuff (i) ; this is not exported! not an interface
  `(incf (x ,i))
  )

(defmethod inc ((i a)) (inc-a-stuff i))
(defmethod inc ((i b)) (incf (y i)))

;; provides a method to generalize back to class a
;; this method does not get further specialization by b, thus
;; remains a window into the "a part"
(defmethod inc-a ((i a)) (inc-a-stuff i))

(defvar r (make-instance 'b))

(inc r) ; all good, increments y

;;(inc (r a)) ; ah how do you get this?
;;
(inc-a r) ; 

(describe r)

#|
Slots with :INSTANCE allocation:
  X  = 1
  Y  = 1
|#

此解决方案对于对象模式的动态更改是无风险的。即它在 CLOS 中工作。

于 2016-02-15T02:52:48.527 回答