11.6 通用函数 (Generic Functions)

一个通用函数 (generic function) 是由一个或多个方法组成的一个函数。方法可用 defmethod 来定义,与 defun 的定义形式类似:

  1. (defmethod combine (x y)
  2. (list x y))

现在 combine 有一个方法。若我们在此时调用 combine ,我们会获得由传入的两个参数所组成的一个列表:

  1. > (combine 'a 'b)
  2. (A B)

到现在我们还没有做任何一般函数做不到的事情。一个通用函数不寻常的地方是,我们可以继续替它加入新的方法。

首先,我们定义一些可以让新的方法引用的类别:

  1. (defclass stuff () ((name :accessor name :initarg :name)))
  2. (defclass ice-cream (stuff) ())
  3. (defclass topping (stuff) ())

这里定义了三个类别: stuff ,只是一个有名字的东西,而 ice-creamtoppingstuff 的子类。

现在下面是替 combine 定义的第二个方法:

  1. (defmethod combine ((ic ice-cream) (top topping))
  2. (format nil "~A ice-cream with ~A topping."
  3. (name ic)
  4. (name top)))

在这次 defmethod 的调用中,参数被特化了 (specialized):每个出现在列表里的参数都有一个类别的名字。一个方法的特化指出它是应用至何种类别的参数。我们刚定义的方法仅能在传给 combine 的参数分别是 ice-creamtopping 的实例时。

而当一个通用函数被调用时, Lisp 是怎么决定要用哪个方法的?Lisp 会使用参数的类别与参数的特化匹配且优先级最高的方法。这表示若我们用 ice-cream 实例与 topping 实例去调用 combine 方法,我们会得到我们刚刚定义的方法:

  1. > (combine (make-instance 'ice-cream :name 'fig)
  2. (make-instance 'topping :name 'treacle))
  3. "FIG ice-cream with TREACLE topping"

但使用其他参数时,我们会得到我们第一次定义的方法:

  1. > (combine 23 'skiddoo)
  2. (23 SKIDDOO)

因为第一个方法的两个参数皆没有特化,它永远只有最低优先权,并永远是最后一个调用的方法。一个未特化的方法是一个安全手段,就像 case 表达式中的 otherwise 子句。

一个方法中,任何参数的组合都可以特化。在这个方法里,只有第一个参数被特化了:

  1. (defmethod combine ((ic ice-cream) x)
  2. (format nil "~A ice-cream with ~A."
  3. (name ic)
  4. x))

若我们用一个 ice-cream 的实例以及一个 topping 的实例来调用 combine ,我们仍然得到特化两个参数的方法,因为它是最具体的那个:

  1. > (combine (make-instance 'ice-cream :name 'grape)
  2. (make-instance 'topping :name 'marshmallow))
  3. "GRAPE ice-cream with MARSHMALLOW topping"

然而若第一个参数是 ice-cream 而第二个参数不是 topping 的实例的话,我们会得到刚刚上面所定义的那个方法:

  1. > (combine (make-instance 'ice-cream :name 'clam)
  2. 'reluctance)
  3. "CLAM ice-cream with RELUCTANCE"

当一个通用函数被调用时,参数决定了一个或多个可用的方法 (applicable methods)。如果在调用中的参数在参数的特化约定内,我们说一个方法是可用的。

如果没有可用的方法,我们会得到一个错误。如果只有一个,它会被调用。如果多于一个,最具体的会被调用。最具体可用的方法是由调用传入参数所属类别的优先级所决定的。由左往右审视参数。如果有一个可用方法的第一个参数,此参数特化给某个类,其类的优先级高于其它可用方法的第一个参数,则此方法就是最具体的可用方法。平手时比较第二个参数,以此类推。 [2]

在前面的例子里,很容易看出哪个是最具体的可用方法,因为所有的对象都是单继承的。一个 ice-cream 的实例是,按顺序来, ice-creamstuffstandard-object , 以及 t 类别的成员。

方法不需要在由 defclass 定义的类别层级来做特化。他们也可以替类型做特化(更精准的说,可以反映出类型的类别)。以下是一个给 combine 用的方法,对数字做了特化:

  1. (defmethod combine ((x number) (y number))
  2. (+ x y))

方法甚至可以对单一的对象做特化,用 eql 来决定:

  1. (defmethod combine ((x (eql 'powder)) (y (eql 'spark)))
  2. 'boom)

单一对象特化的优先级比类别特化来得高。

方法可以像一般 Common Lisp 函数一样有复杂的参数列表,但所有组成通用函数方法的参数列表必须是一致的 (congruent)。参数的数量必须一致,同样数量的选择性参数(如果有的话),要嘛一起使用 &rest 或是 &key 参数,或者一起不要用。下面的参数列表对是全部一致的,

  1. (x) (a)
  2. (x &optional y) (a &optional b)
  3. (x y &rest z) (a b &key c)
  4. (x y &key z) (a b &key c d)

而下列的参数列表对不是一致的:

  1. (x) (a b)
  2. (x &optional y) (a &optional b c)
  3. (x &optional y) (a &rest b)
  4. (x &key x y) (a)

只有必要参数可以被特化。所以每个方法都可以通过名字及必要参数的特化独一无二地识别出来。如果我们定义另一个方法,有着同样的修饰符及特化,它会覆写掉原先的。所以通过说明

  1. (defmethod combine ((x (eql 'powder)) (y (eql 'spark)))
  2. 'kaboom)

我们重定义了当 combine 方法的参数是 powderspark 时, combine 方法干了什么事儿。