17.1 继承 (Inheritance)

11.10 小节解释过通用函数与消息传递的差别。

在消息传递模型里,

  1. 对象有属性,
  2. 并回应消息,
  3. 并从其父类继承属性与方法。

当然了,我们知道 CLOS 使用的是通用函数模型。但本章我们只对于写一个迷你的对象系统 (minimal object system)感兴趣,而不是一个可与 CLOS 匹敌的系统,所以我们将使用消息传递模型。

我们已经在 Lisp 里看过许多保存属性集合的方法。一种可能的方法是使用哈希表来代表对象,并将属性作为哈希表的条目保存。接着可以通过 gethash 来存取每个属性:

  1. (gethash 'color obj)

由于函数是数据对象,我们也可以将函数作为属性保存起来。这表示我们也可以有方法;要调用一个对象特定的方法,可以通过 funcall 一下哈希表里的同名属性:

  1. (funcall (gethash 'move obj) obj 10)

我们可以在这个概念上,定义一个 Smalltalk 风格的消息传递语法,

  1. (defun tell (obj message &rest args)
  2. (apply (gethash message obj) obj args))

所以想要一个对象 obj 移动 10 单位,我们可以说:

  1. (tell obj 'move 10)

事实上,纯 Lisp 唯一缺少的原料是继承。我们可以通过定义一个递归版本的 gethash 来实现一个简单版,如图 17.1 。现在仅用共 8 行代码,便实现了面向对象编程的 3 个基本元素。

  1. (defun rget (prop obj)
  2. (multiple-value-bind (val in) (gethash prop obj)
  3. (if in
  4. (values val in)
  5. (let ((par (gethash :parent obj)))
  6. (and par (rget prop par))))))
  7. (defun tell (obj message &rest args)
  8. (apply (rget message obj) obj args))

图 17.1:继承

让我们用这段代码,来试试本来的例子。我们创建两个对象,其中一个对象是另一个的子类:

  1. > (setf circle-class (make-hash-table)
  2. our-circle (make-hash-table)
  3. (gethash :parent our-circle) circle-class
  4. (gethash 'radius our-circle) 2)
  5. 2

circle-class 对象会持有给所有圆形使用的 area 方法。它是接受一个参数的函数,该参数为传来原始消息的对象:

  1. > (setf (gethash 'area circle-class)
  2. #'(lambda (x)
  3. (* pi (expt (rget 'radius x) 2))))
  4. #<Interpreted-Function BF1EF6>

现在当我们询问 our-circle 的面积时,会根据此类所定义的方法来计算。我们使用 rget 来读取一个属性,用 tell 来调用一个方法:

  1. > (rget 'radius our-circle)
  2. 2
  3. T
  4. > (tell our-circle 'area)
  5. 12.566370614359173

在开始改善这个程序之前,值得停下来想想我们到底做了什么。仅使用 8 行代码,我们使纯的、旧的、无 CLOS 的 Lisp ,转变成一个面向对象语言。我们是怎么完成这项壮举的?应该用了某种秘诀,才会仅用了 8 行代码,就实现了面向对象编程。

的确有一个秘诀存在,但不是编程的奇技淫巧。这个秘诀是,Lisp 本来就是一个面向对象的语言了,甚至说,是种更通用的语言。我们需要做的事情,不过就是把本来就存在的抽象,再重新包装一下。