第十一章:Common Lisp 對(duì)象系統(tǒng)

2018-02-24 15:50 更新

Common Lisp 對(duì)象系統(tǒng),或稱 CLOS,是一組用來實(shí)現(xiàn)面向?qū)ο缶幊痰牟僮骷?。由于它們有著同樣的歷史,通常將這些操作視為一個(gè)群組。?λ?技術(shù)上來說,它們與其他部分的 Common Lisp 沒什么大不同:?defmethod?和?defun?一樣,都是整合在語言中的一個(gè)部分。

11.1 面向?qū)ο缶幊?Object-Oriented Programming

面向?qū)ο缶幊桃馕吨绦蚪M織方式的改變。這個(gè)改變跟已經(jīng)發(fā)生過的處理器運(yùn)算處理能力分配的變化雷同。在 1970 年代,一個(gè)多用戶的計(jì)算機(jī)系統(tǒng)代表著,一個(gè)或兩個(gè)大型機(jī)連接到大量的啞終端(dumb terminal)。現(xiàn)在更可能的是大量相互通過網(wǎng)絡(luò)連接的工作站 (workstation)。系統(tǒng)的運(yùn)算處理能力現(xiàn)在分布至個(gè)體用戶上,而不是集中在一臺(tái)大型的計(jì)算機(jī)上。

面向?qū)ο缶幊趟鶐淼淖兏锱c上例非常類似,前者打破了傳統(tǒng)程序的組織方式。不再讓單一的程序去操作那些數(shù)據(jù),而是告訴數(shù)據(jù)自己該做什么,程序隱含在這些新的數(shù)據(jù)“對(duì)象”的交互過程之中。

舉例來說,假設(shè)我們要算出一個(gè)二維圖形的面積。一個(gè)辦法是寫一個(gè)單獨(dú)的函數(shù),讓它檢查其參數(shù)的類型,然后視類型做處理,如圖 11.1 所示。

(defstruct rectangle
  height width)

(defstruct circle
  radius)

(defun area (x)
  (cond ((rectangle-p x)
         (* (rectangle-height x) (rectangle-width x)))
        ((circle-p x)
         (* pi (expt (circle-radius x) 2)))))

> (let ((r (make-rectangle)))
    (setf (rectangle-height r) 2
          (rectangle-width r) 3)
    (area r))
6

圖 11.1: 使用結(jié)構(gòu)及函數(shù)來計(jì)算面積

使用 CLOS 我們可以寫出一個(gè)等效的程序,如圖 11.2 所示。在面向?qū)ο竽P屠?,我們的程序被拆成?shù)個(gè)獨(dú)一無二的方法,每個(gè)方法為某些特定類型的參數(shù)而生。圖 11.2 中的兩個(gè)方法,隱性地定義了一個(gè)與圖 11.1 相似作用的?area?函數(shù),當(dāng)我們調(diào)用?area?時(shí),Lisp 檢查參數(shù)的類型,并調(diào)用相對(duì)應(yīng)的方法。

(defclass rectangle ()
  (height width))

(defclass circle ()
  (radius))

(defmethod area ((x rectangle))
  (* (slot-value x 'height) (slot-value x 'width)))

(defmethod area ((x circle))
  (* pi (expt (slot-value x 'radius) 2)))

> (let ((r (make-instance 'rectangle)))
    (setf (slot-value r 'height) 2
          (slot-value r 'width) 3)
    (area r))
6

圖 11.2: 使用類型與方法來計(jì)算面積

通過這種方式,我們將函數(shù)拆成獨(dú)一無二的方法,面向?qū)ο蟀抵?em>繼承?(inheritance) ── 槽(slot)與方法(method)皆有繼承。在圖 11.2 中,作為第二個(gè)參數(shù)傳給?defclass?的空列表列出了所有基類。假設(shè)我們要定義一個(gè)新類,上色的圓形 (colored-circle),則上色的圓形有兩個(gè)基類,?colored?與?circle?:

(defclass colored ()
  (color))

(defclass colored-circle (circle colored)
  ())

當(dāng)我們創(chuàng)造?colored-circle?類的實(shí)例 (instance)時(shí),我們會(huì)看到兩個(gè)繼承:

  1. colored-circle?的實(shí)例會(huì)有兩個(gè)槽:從?circle?類繼承而來的?radius?以及從?colored?類繼承而來的?color?。
  2. 由于沒有特別為?colored-circle?定義的?area?方法存在,若我們對(duì)?colored-circle?實(shí)例調(diào)用?area?,我們會(huì)獲得替?circle?類所定義的?area?方法。

從實(shí)踐層面來看,面向?qū)ο缶幊檀碇苑椒?、類、?shí)例以及繼承來組織程序。為什么你會(huì)想這么組織程序?面向?qū)ο蠓椒ǖ闹鲝堉徽f這樣使得程序更容易改動(dòng)。如果我們想要改變?ob?類對(duì)象所顯示的方式,我們只需要改動(dòng)?ob?類的?display?方法。如果我們希望創(chuàng)建一個(gè)新的類,大致上與?ob?相同,只有某些方面不同,我們可以創(chuàng)建一個(gè)?ob?類的子類。在這個(gè)子類里,我們僅改動(dòng)我們想要的屬性,其他所有的屬性會(huì)從?ob?類默認(rèn)繼承得到。要是我們只是想讓某個(gè)?ob?對(duì)象和其他的?ob?對(duì)象不一樣,我們可以新建一個(gè)?ob?對(duì)象,直接修改這個(gè)對(duì)象的屬性即可。若是當(dāng)時(shí)的程序?qū)懙暮苤v究,我們甚至不需要看程序中其他的代碼一眼,就可以完成種種的改動(dòng)。?λ

11.2 類與實(shí)例 (Class and Instances)

在 4.6 節(jié)時(shí),我們看過了創(chuàng)建結(jié)構(gòu)的兩個(gè)步驟:我們調(diào)用?defstruct?來設(shè)計(jì)一個(gè)結(jié)構(gòu)的形式,接著通過一個(gè)像是?make-point?這樣特定的函數(shù)來創(chuàng)建結(jié)構(gòu)。創(chuàng)建實(shí)例 (instances)同樣需要兩個(gè)類似的步驟。首先我們使用?defclass?來定義一個(gè)類別 (Class):

(defclass circle ()
  (radius center))

這個(gè)定義說明了?circle?類別的實(shí)例會(huì)有兩個(gè)槽 (slot),分別名為?radius?與?center?(槽類比于結(jié)構(gòu)里的字段 「field」)。

要?jiǎng)?chuàng)建這個(gè)類的實(shí)例,我們調(diào)用通用的?make-instance?函數(shù),而不是調(diào)用一個(gè)特定的函數(shù),傳入的第一個(gè)參數(shù)為類別名稱:

> (setf c (make-instance 'circle))
#<CIRCLE #XC27496>

要給這個(gè)實(shí)例的槽賦值,我們可以使用?setf?搭配?slot-value?:

> (setf (slot-value c 'radius) 1)
1

與結(jié)構(gòu)的字段類似,未初始化的槽的值是未定義的 (undefined)。

11.3 槽的屬性 (Slot Properties)

傳給?defclass?的第三個(gè)參數(shù)必須是一個(gè)槽定義的列表。如上例所示,最簡(jiǎn)單的槽定義是一個(gè)表示其名稱的符號(hào)。在一般情況下,一個(gè)槽定義可以是一個(gè)列表,第一個(gè)是槽的名稱,伴隨著一個(gè)或多個(gè)屬性 (property)。屬性像關(guān)鍵字參數(shù)那樣指定。

通過替一個(gè)槽定義一個(gè)訪問器 (accessor),我們隱式地定義了一個(gè)可以引用到槽的函數(shù),使我們不需要再調(diào)用?slot-value?函數(shù)。如果我們?nèi)缦赂挛覀兊?circle?類定義,

(defclass circle ()
  ((radius :accessor circle-radius)
   (center :accessor circle-center)))

那我們能夠分別通過?circle-radius?及?circle-center?來引用槽:

> (setf c (make-instance 'circle))
#<CIRCLE #XC5C726>

> (setf (circle-radius c) 1)
1

> (circle-radius c)
1

通過指定一個(gè)?:writer?或是一個(gè)?:reader?,而不是?:accessor?,我們可以獲得訪問器的寫入或讀取行為。

要指定一個(gè)槽的缺省值,我們可以給入一個(gè)?:initform?參數(shù)。若我們想要在?make-instance?調(diào)用期間就將槽初始化,我們可以用:initarg?定義一個(gè)參數(shù)名。?[1]?加入剛剛所說的兩件事,現(xiàn)在我們的類定義變成:

(defclass circle ()
  ((radius :accessor circle-radius
           :initarg :radius
           :initform 1)
   (center :accessor circle-center
           :initarg :center
           :initform (cons 0 0))))

現(xiàn)在當(dāng)我們創(chuàng)建一個(gè)?circle?類的實(shí)例時(shí),我們可以使用關(guān)鍵字參數(shù)?:initarg?給槽賦值,或是將槽的值設(shè)為?:initform?所指定的缺省值。

> (setf c (make-instance 'circle :radius 3))
#<CIRCLE #XC2DE0E>
> (circle-radius c)
3
> (circle-center c)
(0 . 0)

注意?initarg?的優(yōu)先級(jí)比?initform?要高。

我們可以指定某些槽是共享的 ── 也就是每個(gè)產(chǎn)生出來的實(shí)例,共享槽的值都會(huì)是一樣的。我們通過聲明槽擁有?:allocation:class?來辦到此事。(另一個(gè)辦法是讓一個(gè)槽有?:allocation?:instance?,但由于這是缺省設(shè)置,不需要特別再聲明一次。)當(dāng)我們?cè)谝粋€(gè)實(shí)例中,改變了共享槽的值,則其它實(shí)例共享槽也會(huì)獲得相同的值。所以我們會(huì)想要使用共享槽來保存所有實(shí)例都有的相同屬性。

舉例來說,假設(shè)我們想要模擬一群成人小報(bào) (a flock of tabloids)的行為。(譯注:可以看看什么是 tabloids。)在我們的模擬中,我們想要能夠表示一個(gè)事實(shí),也就是當(dāng)一家小報(bào)采用一個(gè)頭條時(shí),其它小報(bào)也會(huì)跟進(jìn)的這個(gè)行為。我們可以通過讓所有的實(shí)例共享一個(gè)槽來實(shí)現(xiàn)。若?tabloid?類別像下面這樣定義,

(defclass tabloid ()
  ((top-story :accessor tabloid-story
              :allocation :class)))

那么如果我們創(chuàng)立兩家小報(bào),無論一家的頭條是什么,另一家的頭條也會(huì)是一樣的:

> (setf daily-blab (make-instance 'tabloid)
        unsolicited-mail (make-instance 'tabloid))
#<TABLOID #x302000EFE5BD>
> (setf (tabloid-story daily-blab) 'adultery-of-senator)
ADULTERY-OF-SENATOR
> (tabloid-story unsolicited-mail)
ADULTERY-OF-SENATOR

譯注: ADULTERY-OF-SENATOR 參議員的性丑聞。

若有給入?:documentation?屬性的話,用來作為?slot?的文檔字符串。通過指定一個(gè)?:type?,你保證一個(gè)槽里只會(huì)有這種類型的元素。類型聲明會(huì)在 13.3 節(jié)講解。

11.4 基類 (Superclasses)

defclass?接受的第二個(gè)參數(shù)是一個(gè)列出其基類的列表。一個(gè)類別繼承了所有基類槽的聯(lián)集。所以要是我們將?screen-circle?定義成circle?與?graphic?的子類,

(defclass graphic ()
  ((color :accessor graphic-color :initarg :color)
   (visible :accessor graphic-visible :initarg :visible
            :initform t)))

(defclass screen-circle (circle graphic) ())

則?screen-circle?的實(shí)例會(huì)有四個(gè)槽,分別從兩個(gè)基類繼承而來。一個(gè)類別不需要自己創(chuàng)建任何新槽;?screen-circle?的存在,只是為了提供一個(gè)可創(chuàng)建同時(shí)從?circle?及?graphic?繼承的實(shí)例。

訪問器及?:initargs?參數(shù)可以用在?screen-circle?的實(shí)例,就如同它們也可以用在?circle?或?graphic?類別那般:

> (graphic-color (make-instance 'screen-circle
                                :color 'red :radius 3))
RED

我們可以使每一個(gè)?screen-circle?有某種缺省的顏色,通過在?defclass?里替這個(gè)槽指定一個(gè)?:initform?:

(defclass screen-circle (circle graphic)
  ((color :initform 'purple)))

現(xiàn)在?screen-circle?的實(shí)例缺省會(huì)是紫色的:

> (graphic-color (make-instance 'screen-circle))
PURPLE

11.5 優(yōu)先級(jí) (Precedence)

我們已經(jīng)看過類別是怎樣能有多個(gè)基類了。當(dāng)一個(gè)實(shí)例的方法同時(shí)屬于這個(gè)實(shí)例所屬的幾個(gè)類時(shí),Lisp 需要某種方式來決定要使用哪個(gè)方法。優(yōu)先級(jí)的重點(diǎn)在于確保這一切是以一種直觀的方式發(fā)生的。

每一個(gè)類別,都有一個(gè)優(yōu)先級(jí)列表:一個(gè)將自身及自身的基類從最具體到最不具體所排序的列表。在目前看過的例子中,優(yōu)先級(jí)還不是需要討論的議題,但在更大的程序里,它會(huì)是一個(gè)需要考慮的議題。

以下是一個(gè)更復(fù)雜的類別層級(jí):

(defclass sculpture () (height width depth))

(defclass statue (sclpture) (subject))

(defclass metalwork () (metal-type))

(defclass casting (metalwork) ())

(defclass cast-statue (statue casting) ())

圖 11.3 包含了一個(gè)表示?cast-statue?類別及其基類的網(wǎng)絡(luò)。

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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)