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

2018-02-24 15:50 更新

Common Lisp 對象系統(tǒng),或稱 CLOS,是一組用來實現(xiàn)面向?qū)ο缶幊痰牟僮骷S捎谒鼈冇兄瑯拥臍v史,通常將這些操作視為一個群組。?λ?技術(shù)上來說,它們與其他部分的 Common Lisp 沒什么大不同:?defmethod?和?defun?一樣,都是整合在語言中的一個部分。

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

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

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

舉例來說,假設(shè)我們要算出一個二維圖形的面積。一個辦法是寫一個單獨的函數(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ù)來計算面積

使用 CLOS 我們可以寫出一個等效的程序,如圖 11.2 所示。在面向?qū)ο竽P屠?,我們的程序被拆成?shù)個獨一無二的方法,每個方法為某些特定類型的參數(shù)而生。圖 11.2 中的兩個方法,隱性地定義了一個與圖 11.1 相似作用的?area?函數(shù),當我們調(diào)用?area?時,Lisp 檢查參數(shù)的類型,并調(diào)用相對應(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: 使用類型與方法來計算面積

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

(defclass colored ()
  (color))

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

當我們創(chuàng)造?colored-circle?類的實例 (instance)時,我們會看到兩個繼承:

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

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

11.2 類與實例 (Class and Instances)

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

(defclass circle ()
  (radius center))

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

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

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

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

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

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

11.3 槽的屬性 (Slot Properties)

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

通過替一個槽定義一個訪問器 (accessor),我們隱式地定義了一個可以引用到槽的函數(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

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

要指定一個槽的缺省值,我們可以給入一個?:initform?參數(shù)。若我們想要在?make-instance?調(diào)用期間就將槽初始化,我們可以用:initarg?定義一個參數(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)在當我們創(chuàng)建一個?circle?類的實例時,我們可以使用關(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)先級比?initform?要高。

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

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

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

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

> (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?的文檔字符串。通過指定一個?:type?,你保證一個槽里只會有這種類型的元素。類型聲明會在 13.3 節(jié)講解。

11.4 基類 (Superclasses)

defclass?接受的第二個參數(shù)是一個列出其基類的列表。一個類別繼承了所有基類槽的聯(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?的實例會有四個槽,分別從兩個基類繼承而來。一個類別不需要自己創(chuàng)建任何新槽;?screen-circle?的存在,只是為了提供一個可創(chuàng)建同時從?circle?及?graphic?繼承的實例。

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

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

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

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

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

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

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

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

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

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

(defclass sculpture () (height width depth))

(defclass statue (sclpture) (subject))

(defclass metalwork () (metal-type))

(defclass casting (metalwork) ())

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

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

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號