第十七章:示例:對象

2018-02-24 15:51 更新

在本章里,我們將使用 Lisp 來自己實現(xiàn)面向對象語言。這樣子的程序稱為嵌入式語言 (embedded language)。嵌入一個面向對象語言到 Lisp 里是一個絕佳的例子。同時作為一個 Lisp 的典型用途,並演示了面向對象的抽象是如何多自然地在 Lisp 基本的抽象上構建出來。

17.1 繼承 (Inheritance)

11.10 小節(jié)解釋過通用函數(shù)與消息傳遞的差別。

在消息傳遞模型里,

  1. 對象有屬性,
  2. 并回應消息,
  3. 并從其父類繼承屬性與方法。

當然了,我們知道 CLOS 使用的是通用函數(shù)模型。但本章我們只對于寫一個迷你的對象系統(tǒng) (minimal object system)感興趣,而不是一個可與 CLOS 匹敵的系統(tǒng),所以我們將使用消息傳遞模型。

我們已經(jīng)在 Lisp 里看過許多保存屬性集合的方法。一種可能的方法是使用哈希表來代表對象,并將屬性作為哈希表的條目保存。接著可以通過?gethash?來存取每個屬性:

(gethash 'color obj)

由于函數(shù)是數(shù)據(jù)對象,我們也可以將函數(shù)作為屬性保存起來。這表示我們也可以有方法;要調(diào)用一個對象特定的方法,可以通過funcall?一下哈希表里的同名屬性:

(funcall (gethash 'move obj) obj 10)

我們可以在這個概念上,定義一個 Smalltalk 風格的消息傳遞語法,

(defun tell (obj message &rest args)
  (apply (gethash message obj) obj args))

所以想要一個對象?obj?移動 10 單位,我們可以說:

(tell obj 'move 10)

事實上,純 Lisp 唯一缺少的原料是繼承。我們可以通過定義一個遞歸版本的?gethash?來實現(xiàn)一個簡單版,如圖 17.1 ?,F(xiàn)在僅用共 8 行代碼,便實現(xiàn)了面向對象編程的 3 個基本元素。

(defun rget (prop obj)
  (multiple-value-bind (val in) (gethash prop obj)
    (if in
        (values val in)
        (let ((par (gethash :parent obj)))
          (and par (rget prop par))))))

(defun tell (obj message &rest args)
  (apply (rget message obj) obj args))

圖 17.1:繼承

讓我們用這段代碼,來試試本來的例子。我們創(chuàng)建兩個對象,其中一個對象是另一個的子類:

> (setf circle-class (make-hash-table)
        our-circle   (make-hash-table)
        (gethash :parent our-circle) circle-class
        (gethash 'radius our-circle) 2)
2

circle-class?對象會持有給所有圓形使用的?area?方法。它是接受一個參數(shù)的函數(shù),該參數(shù)為傳來原始消息的對象:

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

現(xiàn)在當我們詢問?our-circle?的面積時,會根據(jù)此類所定義的方法來計算。我們使用?rget?來讀取一個屬性,用?tell?來調(diào)用一個方法:

> (rget 'radius our-circle)
2
T
> (tell our-circle 'area)
12.566370614359173

在開始改善這個程序之前,值得停下來想想我們到底做了什么。僅使用 8 行代碼,我們使純的、舊的、無 CLOS 的 Lisp ,轉變成一個面向對象語言。我們是怎么完成這項壯舉的?應該用了某種秘訣,才會僅用了 8 行代碼,就實現(xiàn)了面向對象編程。

的確有一個秘訣存在,但不是編程的奇技淫巧。這個秘訣是,Lisp 本來就是一個面向對象的語言了,甚至說,是種更通用的語言。我們需要做的事情,不過就是把本來就存在的抽象,再重新包裝一下。

17.2 多重繼承 (Multiple Inheritance)

到目前為止我們只有單繼承 ── 一個對象只可以有一個父類。但可以通過使?parent?屬性變成一個列表來獲得多重繼承,并重新定義?rget?,如圖 17.2 所示。

在只有單繼承的情況下,當我們想要從對象取出某些屬性,只需要遞歸地延著祖先的方向往上找。如果對象本身沒有我們想要屬性的有關信息,可以檢視其父類,以此類推。有了多重繼承后,我們?nèi)韵胍獔?zhí)行同樣的搜索,但這件簡單的事,卻被對象的祖先可形成一個圖,而不再是簡單的樹給復雜化了。不能只使用深度優(yōu)先來搜索這個圖。有多個父類時,可以有如圖 17.3 所示的層級存在:?a?起源于?b?及?c?,而他們都是?d?的子孫。一個深度優(yōu)先(或說高度優(yōu)先)的遍歷結果會是?a?,?b?,?d,?c?,?d?。而如果我們想要的屬性在?d與?c?都有的話,我們會獲得存在?d?的值,而不是存在?c?的值。這違反了子類可覆寫父類提供缺省值的原則。

如果我們想要實現(xiàn)普遍的繼承概念,就不應該在檢查其子孫前,先檢查該對象。在這個情況下,適當?shù)乃阉黜樞驎?a?,?b?,?c?,?d?。那如何保證搜索總是先搜子孫呢?最簡單的方法是用一個對象,以及按正確優(yōu)先順序排序的,由祖先所構成的列表。通過調(diào)用traverse?開始,建構一個列表,表示深度優(yōu)先遍歷所遇到的對象。如果任一個對象有共享的父類,則列表中會有重復元素。如果僅保存最后出現(xiàn)的復本,會獲得一般由 CLOS 定義的優(yōu)先級列表。(刪除所有除了最后一個之外的復本,根據(jù) 183 頁所描述的算法,規(guī)則三。)Common Lisp 函數(shù)?delete-duplicates?定義成如此作用的,所以我們只要在深度優(yōu)先的基礎上調(diào)用它,我們就會得到正確的優(yōu)先級列表。一旦優(yōu)先級列表創(chuàng)建完成,?rget?根據(jù)需要的屬性搜索第一個符合的對象。

我們可以通過利用優(yōu)先級列表的優(yōu)點,舉例來說,一個愛國的無賴先是一個無賴,然后才是愛國者:

> (setf scoundrel           (make-hash-table)
        patriot             (make-hash-table)
        patriotic-scoundrel (make-hash-table)
        (gethash 'serves scoundrel) 'self
        (gethash 'serves patriot) 'country
        (gethash :parents patriotic-scoundrel)
                 (list scoundrel patriot))
(#<Hash-Table C41C7E> #<Hash-Table C41F0E>)
> (rget 'serves patriotic-scoundrel)
SELF
T

到目前為止,我們有一個強大的程序,但極其丑陋且低效。在一個 Lisp 程序生命周期的第二階段,我們將這個初步框架提煉成有用的東西。

17.3 定義對象 (Defining Objects)

第一個我們需要改善的是,寫一個用來創(chuàng)建對象的函數(shù)。我們程序表示對象以及其父類的方式,不需要給用戶知道。如果我們定義一個函數(shù)來創(chuàng)建對象,用戶將能夠一個步驟就創(chuàng)建出一個對象,并指定其父類。我們可以在創(chuàng)建一個對象的同時,順道構造優(yōu)先級列表,而不是在每次當我們需要找一個屬性或方法時,才花費龐大代價來重新構造。

如果我們要維護優(yōu)先級列表,而不是在要用的時候再構造它們,我們需要處理列表會過時的可能性。我們的策略會是用一個列表來保存所有存在的對象,而無論何時當某些父類被改動時,重新給所有受影響的對象生成優(yōu)先級列表。這代價是相當昂貴的,但由于查詢比重定義父類的可能性來得高許多,我們會省下許多時間。這個改變對我們的程序的靈活性沒有任何影響;我們只是將花費從頻繁的操作轉到不頻繁的操作。

圖 17.4 包含了新的代碼。?λ?全局的?*objs*?會是一個包含所有當前對象的列表。函數(shù)?parents?取出一個對象的父類;相反的?(setfparents)?不僅配置一個對象的父類,也調(diào)用?make-precedence?來重新構造任何需要變動的優(yōu)先級列表。這些列表與之前一樣,由precedence?來構造。

用戶現(xiàn)在不用調(diào)用?make-hash-table?來創(chuàng)建對象,調(diào)用?obj?來取代,?obj?一步完成創(chuàng)建一個新對象及定義其父類。我們也重定義了rget?來利用保存優(yōu)先級列表的好處。

(defvar *objs* nil)

(defun parents (obj) (gethash :parents obj))

(defun (setf parents) (val obj)
  (prog1 (setf (gethash :parents obj) val)
         (make-precedence obj)))

(defun make-precedence (obj)
  (setf (gethash :preclist obj) (precedence obj))
  (dolist (x *objs*)
    (if (member obj (gethash :preclist x))
        (setf (gethash :preclist x) (precedence x)))))

(defun obj (&rest parents)
  (let ((obj (make-hash-table)))
    (push obj *objs*)
    (setf (parents obj) parents)
    obj))

(defun rget (prop obj)
  (dolist (c (gethash :preclist obj))
    (multiple-value-bind (val in) (gethash prop c)
      (if in (return (values val in))))))

圖 17.4:創(chuàng)建對象

17.4 函數(shù)式語法 (Functional Syntax)

另一個可以改善的空間是消息調(diào)用的語法。?tell?本身是無謂的雜亂不堪,這也使得動詞在第三順位才出現(xiàn),同時代表著我們的程序不再可以像一般 Lisp 前序表達式那樣閱讀:

(tell (tell obj 'find-owner) 'find-owner)

我們可以使用圖 17.5 所定義的?defprop?宏,通過定義作為函數(shù)的屬性名稱來擺脫這種?tell?語法。若選擇性參數(shù)?meth??為真的話,會將此屬性視為方法。不然會將屬性視為槽,而由?rget?所取回的值會直接返回。一旦我們定義了屬性作為槽或方法的名字,

(defmacro defprop (name &optional meth?)
  `(progn
     (defun ,name (obj &rest args)
       ,(if meth?
          `(run-methods obj ',name args)
          `(rget ',name obj)))
     (defun (setf ,name) (val obj)
       (setf (gethash ',name obj) val))))

(defun run-methods (obj name args)
  (let ((meth (rget name obj)))
    (if meth
        (apply meth obj args)
        (error "No ~A method for ~A." name obj))))

圖 17.5: 函數(shù)式語法

(defprop find-owner t)

我們就可以在函數(shù)調(diào)用里引用它,則我們的代碼讀起來將會再次回到 Lisp 本來那樣:

(find-owner (find-owner obj))

我們的前一個例子在某種程度上可讀性變得更高了:

> (progn
    (setf scoundrel           (obj)
          patriot             (obj)
          patriotic-scoundrel (obj scoundrel patriot))
    (defprop serves)
    (setf (serves scoundrel) 'self
          (serves patriot) 'country)
    (serves patriotic-scoundrel))
SELF
T

17.5 定義方法 (Defining Methods)

到目前為止,我們借由敘述如下的東西來定義一個方法:

(defprop area t)

(setf circle-class (obj))

(setf (area circle-class)
      #'(lambda (c) (* pi (expt (radius c) 2))))
(defmacro defmeth (name obj parms &rest body)
  (let ((gobj (gensym)))
    `(let ((,gobj ,obj))
       (setf (gethash ',name ,gobj)
             (labels ((next () (get-next ,gobj ',name)))
               #'(lambda ,parms ,@body))))))

(defun get-next (obj name)
  (some #'(lambda (x) (gethash name x))
        (cdr (gethash :preclist obj))))

圖 17.6 定義方法。

在一個方法里,我們可以通過給對象的?:preclist?的?cdr?獲得如內(nèi)置?call-next-method?方法的效果。所以舉例來說,若我們想要定義一個特殊的圓形,這個圓形在返回面積的過程中印出某個東西,我們可以說:

(setf grumpt-circle (obj circle-class))

(setf (area grumpt-circle)
      #'(lambda (c)
          (format t "How dare you stereotype me!~%")
          (funcall (some #'(lambda (x) (gethash 'area x))
                         (cdr (gethash :preclist c)))
                   c)))

這里?funcall?等同于一個?call-next-method?調(diào)用,但他..

圖 17.6 的?defmeth?宏提供了一個便捷方式來定義方法,并使得調(diào)用下個方法變得簡單。一個?defmeth?的調(diào)用會展開成一個?setf?表達式,但?setf?在一個?labels?表達式里定義了?next?作為取出下個方法的函數(shù)。這個函數(shù)與?next-method-p?類似(第 188 頁「譯註: 11.7 節(jié)」),但返回的是我們可以調(diào)用的東西,同時作為?call-next-method?。?λ?前述兩個方法可以被定義成:

(defmeth area circle-class (c)
  (* pi (expt (radius c) 2)))

(defmeth area grumpy-circle (c)
  (format t "How dare you stereotype me!~%")
  (funcall (next) c))

順道一提,注意?defmeth?的定義也利用到了符號捕捉。方法的主體被插入至函數(shù)?next?是局部定義的一個上下文里。

17.6 實例 (Instances)

到目前為止,我們還沒有將類別與實例做區(qū)別。我們使用了一個術語來表示兩者,對象(object)。將所有的對象視為一體是優(yōu)雅且靈活的,但這非常沒效率。在許多面向對象應用里,繼承圖的底部會是復雜的。舉例來說,模擬一個交通情況,我們可能有少于十個對象來表示車子的種類,但會有上百個對象來表示特定的車子。由于后者會全部共享少數(shù)的優(yōu)先級列表,創(chuàng)建它們是浪費時間的,并且浪費空間來保存它們。

圖 17.7 定義一個宏?inst?,用來創(chuàng)建實例。實例就像其他對象一樣(現(xiàn)在也可稱為類別),有區(qū)別的是只有一個父類且不需維護優(yōu)先級列表。它們也沒有包含在列表?*objs**?里。在前述例子里,我們可以說:

(setf grumpy-circle (inst circle-class))

由于某些對象不再有優(yōu)先級列表,函數(shù)?rget?以及?get-next?現(xiàn)在被重新定義,檢查這些對象的父類來取代。獲得的效率不用拿靈活性交換。我們可以對一個實例做任何我們可以給其它種對象做的事,包括創(chuàng)建一個實例以及重定義其父類。在后面的情況里,?(setfparents)?會有效地將對象轉換成一個“類別”。

17.7 新的實現(xiàn) (New Implementation)

我們到目前為止所做的改善都是犧牲靈活性交換而來。在這個系統(tǒng)的開發(fā)后期,一個 Lisp 程序通常可以犧牲些許靈活性來獲得好處,這里也不例外。目前為止我們使用哈希表來表示所有的對象。這給我們帶來了超乎我們所需的靈活性,以及超乎我們所想的花費。在這個小節(jié)里,我們會重寫我們的程序,用簡單向量來表示對象。

(defun inst (parent)
  (let ((obj (make-hash-table)))
    (setf (gethash :parents obj) parent)
    obj))

(defun rget (prop obj)
  (let ((prec (gethash :preclist obj)))
    (if prec
        (dolist (c prec)
          (multiple-value-bind (val in) (gethash prop c)
            (if in (return (values val in)))))
      (multiple-value-bind (val in) (gethash prop obj)
        (if in
            (values val in)
            (rget prop (gethash :parents obj)))))))

(defun get-next (obj name)
  (let ((prec (gethash :preclist obj)))
    (if prec
        (some #'(lambda (x) (gethash name x))
              (cdr prec))
      (get-next (gethash obj :parents) name))))

圖 17.7: 定義實例

這個改變意味著放棄動態(tài)定義新屬性的可能性。目前我們可通過引用任何對象,給它定義一個屬性?,F(xiàn)在當一個類別被創(chuàng)建時,我們會需要給出一個列表,列出該類有的新屬性,而當實例被創(chuàng)建時,他們會恰好有他們所繼承的屬性。

在先前的實現(xiàn)里,類別與實例沒有實際區(qū)別。一個實例只是一個恰好有一個父類的類別。如果我們改動一個實例的父類,它就變成了一個類別。在新的實現(xiàn)里,類別與實例有實際區(qū)別;它使得將實例轉成類別不再可能。

在圖 17.8-17.10 的代碼是一個完整的新實現(xiàn)。圖片 17.8 給創(chuàng)建類別與實例定義了新的操作符。類別與實例用向量來表示。表示類別與實例的向量的前三個元素包含程序自身要用到的信息,而圖 17.8 的前三個宏是用來引用這些元素的:

(defmacro parents (v) `(svref ,v 0))
(defmacro layout (v) `(the simple-vector (svref ,v 1)))
(defmacro preclist (v) `(svref ,v 2))

(defmacro class (&optional parents &rest props)
  `(class-fn (list ,@parents) ',props))

(defun class-fn (parents props)
  (let* ((all (union (inherit-props parents) props))
         (obj (make-array (+ (length all) 3)
                          :initial-element :nil)))
    (setf (parents obj)  parents
          (layout obj)   (coerce all 'simple-vector)
          (preclist obj) (precedence obj))
    obj))

(defun inherit-props (classes)
  (delete-duplicates
    (mapcan #'(lambda (c)
                (nconc (coerce (layout c) 'list)
                       (inherit-props (parents c))))
            classes)))

(defun precedence (obj)
  (labels ((traverse (x)
             (cons x
                   (mapcan #'traverse (parents x)))))
    (delete-duplicates (traverse obj))))

(defun inst (parent)
  (let ((obj (copy-seq parent)))
    (setf (parents obj)  parent
          (preclist obj) nil)
    (fill obj :nil :start 3)
    obj))

圖 17.8: 向量實現(xiàn):創(chuàng)建

  1. parents?字段取代舊實現(xiàn)中,哈希表條目里?:parents?的位置。在一個類別里,?parents?會是一個列出父類的列表。在一個實例里,?parents?會是一個單一的父類。
  2. layout?字段是一個包含屬性名字的向量,指出類別或實例的從第四個元素開始的設計 (layout)。
  3. preclist?字段取代舊實現(xiàn)中,哈希表條目里?:preclist?的位置。它會是一個類別的優(yōu)先級列表,實例的話就是一個空表。

因為這些操作符是宏,他們?nèi)伎梢员?setf?的第一個參數(shù)使用(參考 10.6 節(jié))。

class?宏用來創(chuàng)建類別。它接受一個含有其基類的選擇性列表,伴隨著零個或多個屬性名稱。它返回一個代表類別的對象。新的類別會同時有自己本身的屬性名,以及從所有基類繼承而來的屬性。

> (setf *print-array* nil
        gemo-class (class nil area)
        circle-class (class (geom-class) radius))
#<Simple-Vector T 5 C6205E>

這里我們創(chuàng)建了兩個類別:?geom-class?沒有基類,且只有一個屬性,?area?;?circle-class?是?gemo-class?的子類,并添加了一個屬性,?radius?。?[1]?circle-class?類的設計

> (coerce (layout circle-class) 'list)
(AREA RADIUS)

顯示了五個字段里,最后兩個的名稱。?[2]

class?宏只是一個?class-fn?的介面,而?class-fn?做了實際的工作。它調(diào)用?inherit-props?來匯整所有新對象的父類,匯整成一個列表,創(chuàng)建一個正確長度的向量,并適當?shù)嘏渲们叭齻€字段。(?preclist?由?precedence?創(chuàng)建,本質上?precedence?沒什么改變。)類別余下的字段設置為?:nil?來指出它們尚未初始化。要檢視?circle-class?的?area?屬性,我們可以:

> (svref circle-class
         (+ (position 'area (layout circle-class)) 3))
:NIL

稍后我們會定義存取函數(shù)來自動辦到這件事。

最后,函數(shù)?inst?用來創(chuàng)建實例。它不需要是一個宏,因為它僅接受一個參數(shù):

> (setf our-circle (inst circle-class))
#<Simple-Vector T 5 C6464E>

比較?inst?與?class-fn?是有益學習的,它們做了差不多的事。因為實例僅有一個父類,不需要決定它繼承什么屬性。實例可以僅拷貝其父類的設計。它也不需要構造一個優(yōu)先級列表,因為實例沒有優(yōu)先級列表。創(chuàng)建實例因此與創(chuàng)建類別比起來來得快許多,因為創(chuàng)建實例在多數(shù)應用里比創(chuàng)建類別更常見。

(declaim (inline lookup (setf lookup)))

(defun rget (prop obj next?)
  (let ((prec (preclist obj)))
    (if prec
        (dolist (c (if next? (cdr prec) prec) :nil)
          (let ((val (lookup prop c)))
            (unless (eq val :nil) (return val))))
        (let ((val (lookup prop obj)))
          (if (eq val :nil)
              (rget prop (parents obj) nil)
              val)))))

(defun lookup (prop obj)
  (let ((off (position prop (layout obj) :test #'eq)))
    (if off (svref obj (+ off 3)) :nil)))

(defun (setf lookup) (val prop obj)
  (let ((off (position prop (layout obj) :test #'eq)))
    (if off
        (setf (svref obj (+ off 3)) val)
        (error "Can't set ~A of ~A." val obj))))

圖 17.9: 向量實現(xiàn):存取

現(xiàn)在我們可以創(chuàng)建所需的類別層級及實例,以及需要的函數(shù)來讀寫它們的屬性。圖 17.9 的第一個函數(shù)是?rget?的新定義。它的形狀與圖 17.7 的?rget?相似。條件式的兩個分支,分別處理類別與實例。

  1. 若對象是一個類別,我們遍歷其優(yōu)先級列表,直到我們找到一個對象,其中欲找的屬性不是?:nil?。如果沒有找到,返回?:nil。
  2. 若對象是一個實例,我們直接查找屬性,并在沒找到時遞回地調(diào)用?rget?。

rget?與?next??新的第三個參數(shù)稍后解釋?,F(xiàn)在只要了解如果是?nil?,?rget?會像平常那樣工作。

函數(shù)?lookup?及其反相扮演著先前?rget?函數(shù)里?gethash?的角色。它們使用一個對象的?layout?,來取出或設置一個給定名稱的屬性。這條查詢是先前的一個復本:

> (lookup 'area circle-class)
:NIL

由于?lookup?的?setf?也定義了,我們可以給?circle-class?定義一個?area?方法,通過:

(setf (lookup 'area circle-class)
      #'(lambda (c)
          (* pi (expt (rget 'radius c nil) 2))))

在這個程序里,和先前的版本一樣,沒有特別區(qū)別出方法與槽。一個“方法”只是一個字段,里面有著一個函數(shù)。這將很快會被一個更方便的前端所隱藏起來。

(declaim (inline run-methods))

(defmacro defprop (name &optional meth?)
  `(progn
     (defun ,name (obj &rest args)
       ,(if meth?
            `(run-methods obj ',name args)
            `(rget ',name obj nil)))
     (defun (setf ,name) (val obj)
       (setf (lookup ',name obj) val))))

(defun run-methods (obj name args)
  (let ((meth (rget name obj nil)))
    (if (not (eq meth :nil))
        (apply meth obj args)
        (error "No ~A method for ~A." name obj))))

(defmacro defmeth (name obj parms &rest body)
  (let ((gobj (gensym)))
    `(let ((,gobj ,obj))
       (defprop ,name t)
       (setf (lookup ',name ,gobj)
             (labels ((next () (rget ,gobj ',name t)))
               #'(lambda ,parms ,@body))))))

圖 17.10: 向量實現(xiàn):宏介面

圖 17.10 包含了新的實現(xiàn)的最后部分。這個代碼沒有給程序加入任何威力,但使程序更容易使用。宏?defprop?本質上沒有改變;現(xiàn)在僅調(diào)用?lookup?而不是?gethash?。與先前相同,它允許我們用函數(shù)式的語法來引用屬性:

> (defprop radius)
(SETF RADIUS)
> (radius our-circle)
:NIL
> (setf (radius our-circle) 2)
2

如果?defprop?的第二個選擇性參數(shù)為真的話,它展開成一個?run-methods?調(diào)用,基本上也沒什么改變。

最后,函數(shù)?defmeth?提供了一個便捷方式來定義方法。這個版本有三件新的事情:它隱含了?defprop?,它調(diào)用?lookup?而不是gethash?,且它調(diào)用?regt?而不是 278 頁的?get-next?(譯注: 圖 17.7 的?get-next?)來獲得下個方法?,F(xiàn)在我們理解給?rget?添加額外參數(shù)的理由。它與?get-next?非常相似,我們同樣通過添加一個額外參數(shù),在一個函數(shù)里實現(xiàn)。若這額外參數(shù)為真時,?rget?取代get-next?的位置。

現(xiàn)在我們可以達到先前方法定義所有的效果,但更加清晰:

(defmeth area circle-class (c)
  (* pi (expt (radius c) 2)))

注意我們可以直接調(diào)用?radius?而無須調(diào)用?rget?,因為我們使用?defprop?將它定義成一個函數(shù)。因為隱含的?defprop?由?defmeth實現(xiàn),我們也可以調(diào)用?area?來獲得?our-circle?的面積:

> (area our-circle)
12.566370614359173

17.8 分析 (Analysis)

我們現(xiàn)在有了一個適合撰寫實際面向對象程序的嵌入式語言。它很簡單,但就大小來說相當強大。而在典型應用里,它也會是快速的。在一個典型的應用里,操作實例應比操作類別更常見。我們重新設計的重點在于如何使得操作實例的花費降低。

在我們的程序里,創(chuàng)建類別既慢且產(chǎn)生了許多垃圾。如果類別不是在速度為關鍵考量時創(chuàng)建,這還是可以接受的。會需要速度的是存取函數(shù)以及創(chuàng)建實例。這個程序里的沒有做編譯優(yōu)化的存取函數(shù)大約與我們預期的一樣快。?λ?而創(chuàng)建實例也是如此。且兩個操作都沒有用到構造 (consing)。除了用來表達實例的向量例外。會自然的以為這應該是動態(tài)地配置才對。但我們甚至可以避免動態(tài)配置實例,如果我們使用像是 13.4 節(jié)所提出的策略。

我們的嵌入式語言是 Lisp 編程的一個典型例子。只不過是一個嵌入式語言就可以是一個例子了。但 Lisp 的特性是它如何從一個小的、受限版本的程序,進化成一個強大但低效的版本,最終演化成快速但稍微受限的版本。

Lisp 惡名昭彰的緩慢不是 Lisp 本身導致(Lisp 編譯器早在 1980 年代就可以產(chǎn)生出與 C 編譯器一樣快的代碼),而是由于許多程序員在第二個階段就放棄的事實。如同 Richard Gabriel 所寫的,

要在 Lisp 撰寫出性能極差的程序相當簡單;而在 C 這幾乎是不可能的。?λ

這完全是一個真的論述,但也可以解讀為贊揚或貶低 Lisp 的論點:

  1. 通過犧牲靈活性換取速度,你可以在 Lisp 里輕松地寫出程序;在 C 語言里,你沒有這個選擇。
  2. 除非你優(yōu)化你的 Lisp 代碼,不然要寫出緩慢的軟件根本易如反掌。

你的程序屬于哪一種解讀完全取決于你。但至少在開發(fā)初期,Lisp 使你有犧牲執(zhí)行速度來換取時間的選擇。

有一件我們示例程序沒有做的很好的事是,它不是一個稱職的 CLOS 模型(除了可能沒有說明難以理解的?call-next-method?如何工作是件好事例外)。如大象般龐大的 CLOS 與這個如蚊子般微小的 70 行程序之間,存在多少的相似性呢?當然,這兩者的差別是出自于教育性,而不是探討有多相似。首先,這使我們理解到“面向對象”的廣度。我們的程序比任何被稱為是面向對象的都來得強大,而這只不過是 CLOS 的一小部分威力。

我們程序與 CLOS 不同的地方是,方法是屬于某個對象的。這個方法的概念使它們與對第一個參數(shù)做派發(fā)的函數(shù)相同。而當我們使用函數(shù)式語法來調(diào)用方法時,這看起來就跟 Lisp 的函數(shù)一樣。相反地,一個 CLOS 的通用函數(shù),可以派發(fā)它的任何參數(shù)。一個通用函數(shù)的組件稱為方法,而若你將它們定義成只對第一個參數(shù)特化,你可以制造出它們是某個類或實例的方法的錯覺。但用面向對象編程的消息傳遞模型來思考 CLOS 最終只會使你困惑,因為 CLOS 凌駕在面向對象編程之上。

CLOS 的缺點之一是它太龐大了,并且 CLOS 費煞苦心的隱藏了面向對象編程,其實只不過是改寫 Lisp 的這個事實。本章的例子至少闡明了這一點。如果我們滿足于舊的消息傳遞模型,我們可以用一頁多一點的代碼來實現(xiàn)。面向對象編程不過是 Lisp 可以做的小事之一而已。更發(fā)人深省的問題是,Lisp 除此之外還能做些什么?

腳注

[1] | 當類別被顯示時,?*print-array*?應當是?nil?。 任何類別的?preclist?的第一個元素都是類別本身,所以試圖顯示類別的內(nèi)部結構會導致一個無限循環(huán)。

[2] | 這個向量被 coerced 成一個列表,只是為了看看里面有什么。有了?*print-array*?被設成?nil?,一個向量的內(nèi)容應該不會顯示出來。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號