跳轉到內容

通用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 類中每個槽的情況)。簡而言之,只提供那些可能對您有用的選項。

華夏公益教科書