Common Lisp 對(duì)象系統(tǒng),或稱 CLOS,是一組用來實(shí)現(xiàn)面向?qū)ο缶幊痰牟僮骷?。由于它們有著同樣的歷史,通常將這些操作視為一個(gè)群組。?λ?技術(shù)上來說,它們與其他部分的 Common Lisp 沒什么大不同:?defmethod
?和?defun
?一樣,都是整合在語言中的一個(gè)部分。
面向?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è)繼承:
colored-circle
?的實(shí)例會(huì)有兩個(gè)槽:從?circle
?類繼承而來的?radius
?以及從?colored
?類繼承而來的?color
?。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)。?λ
在 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)。
傳給?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é)講解。
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
我們已經(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ò)。
更多建議: