通用Lisp/高階主題/CLOS
通用Lisp物件系統 (CLOS) 是通用Lisp的一部分,它允許在程式中使用面向物件程式設計技術。它定義了諸如物件、類和方法以及它們之間的互動等概念。CLOS 是任何程式語言中最強大的物件系統,掌握其更奇特的方面可能需要時間。幸運的是,不必成為 CLOS 專家就能使用它。
CLOS 的兩個正交概念是類和泛型函式。讓我們從第一個開始...
類是一個描述其例項的結構和行為的“模板”。每種Lisp資料都是某個類的例項。有一些內建類,比如整數類或字串類。可以使用class-of 函式來確定某個Lisp物件的類。
(class-of 5) => #<BUILT-IN-CLASS INTEGER> (class-of "aaaa") => #<BUILT-IN-CLASS STRING>
結果可能在您的特定實現中以不同的方式列印,但想法是一樣的:#< > 是Lisp語法,表示不可讀資料,這並不意味著它對人類不可讀,而是對Lisp閱讀器不可讀。很容易確定 5 是內建類integer 的例項,而 "aaaa" 是內建類string 的例項。
內建類通常不是很有趣。但是,有一種方法可以建立使用者定義的類,並且可以透過defclass 宏來實現。使用者定義類的每個例項都將具有一定數量的槽,這些槽可以包含各種Lisp資料。讓我們看看 defclass 的典型用法
(defclass book ()
((author :initarg :author :initform "" :accessor author)
(title :initarg :title :initform "" :accessor title)
(year :initarg :year :initform 0 :accessor year))
(:documentation "Describes a book"))
它具有以下結構
(defclass name (superclasses) (slots) options)
我們現在將忽略超類,讓我們看一下槽。每個槽都有一個名稱和幾個附加到它的選項。這些選項是
:initarg- 一個關鍵字,它將在建立類例項時用於提供槽值。:initform- 如果沒有為槽提供值,它將使用 initform 的計算結果進行初始化。如果沒有 initform,將發出錯誤訊號。:reader- 指定一個函式來讀取特定槽。:reader aaa表示:建立一個函式aaa,以便 (aaa instance) 返回槽的值。:writer- 指定一個函式來寫入特定槽。:writer bbb表示:建立一個函式bbb,以便 (bbb value instance) 將例項的槽設定為 value。:accessor- 指定一個函式來讀取和寫入槽的值。:accessor foo表示:建立一個函式foo 並建立一個函式(setf foo),以便 (foo instance) 讀取槽的值,而 (setf (foo instance) value) 設定槽的值。- 注意:
:reader foo :writer foo是錯誤的!讀者和作者需要不同的名稱。請改用:accessor foo。
- 注意:
:documentation- 為特定槽提供文件字串。
每個選項都是完全可選的,但至少應該提供:initarg 或:initform 中的一個,以便能夠在物件建立時初始化槽。未能做到這一點會導致執行時錯誤。
至於類選項,唯一有用的選項是:documentation,它為整個類提供文件字串。
現在我們有了類,我們可以建立一些它的例項。Lisp 中的一切都是物件,但標準類的例項(如上面由defclass 定義的類)被稱為標準物件。在本節的剩餘部分,物件一詞指的是標準物件。
如何建立一個新物件?我們需要一個函式make-instance
(setf *my-book* (make-instance 'book))
現在 *my-book*' 的值為類book 的一個物件。make-instance 的第一個引數可以計算為類本身(例如class-of 呼叫結果)或命名類的符號(例如引用此符號的結果)。在這種情況下,我們將使用符號,這更容易生成。
讓我們看看 *my-book*
(class-of *my-book*) => #<STANDARD-CLASS BOOK> (author *my-book*) => "" (year *my-book*) => 0
這本書現在相當平淡。它的欄位被設定為預設的類值,但很容易更改它們
(setf (title *my-book*) "ANSI Common Lisp") (setf (author *my-book*) "Paul Graham") (setf (year *my-book*) 1995)
這是因為在類定義中設定了適當的訪問器函式。在一般情況下,訪問物件的槽比較困難。例如,如果只定義了讀取器函式,您仍然可以使用slot-value 函式來更改槽
(setf (slot-value *my-book* 'year) 1995) (year *my-book*) => 1995
您也可以丟棄讀取器函式,並使用slot-value 函式來讀取槽值。但是,這不會增加程式碼的可讀性。
大多數情況下,您不希望使用所有槽都設定為預設值的建立物件。例如,如果要建立一個表示特定書籍的物件,您已經知道它的標題、作者和年份,並且您想使用這些值初始化一個新物件,而不是一些無用的空字串。如果在槽定義中指定了 :initarg 選項,則可以在物件建立時使用使用者指定的引數初始化此槽。可以透過向make-instance 提供相應的關鍵字引數來實現
(make-instance 'book
:author "Paul Graham"
:title "ANSI Common Lisp"
:year 1995)
由於book 槽的預設值始終是無用的(沒有年份 0,並且沒有標題為空的書),因此應該刪除 :initform 槽選項。然後,忘記初始化槽的使用者將收到錯誤訊息,並且將在下次提供所有必要資訊以解決此問題。明智的做法是隻為不打算更改的槽提供 :reader 函式(這是我們book 類中每個槽的情況)。簡而言之,只提供那些可能對您有用的選項。