第十二章 結構體和類型系統(Structures and The Type System)
12.1 導語
Common Lisp包含了很多內建的數據類型,他們一起形成了一個類型系統。我們到現在為止學到的類型有數字(包括一些變種),字符,組合(conses),字符串,函數對象還有流對象。這些都是基本數據類型,但是還有更多。
Common Lisp類型系統有兩個重要的屬性,第一,類型是可見的,他們由lisp數據結構來描述(字符和列表),并且還有內建參數來測試對象的類型,返回對象的類型描述。第二,類型系統是可擴展的,程序員可以再任何時候創造新的類型。
結構體(structures)是一個程序員定義數據類型的模板。本章在介紹數據類型系統的基礎之后,會解釋新的結構體類型是如何被定義,結構體又是如何被創造和修改的。
Common Lisp對象系統(object system)(CLOS)提供了一個高級的程序員定義的數據類型容器來支持面向對象的編程風格。我們不會在本書中介紹CLOS,以我們的墓地來說,結構體已經足夠了。
12.2 TYPEP和TYPE-OF
斷言typep,假如一個對象是特定類型的對象之一,就會返回true。類型定義可能回事一個非常復雜的表述,這里我們只看簡單的情況。
函數type-of返回一個對象的類型定義。因為對象可以屬于多個類型,3是一個數字number,又是一個整數integer。nil既是一個字符優勢一個列表。所以具體返回的結果是根據不同的lisp實現而決定的。
類型定義(SIMPLE-STRING 6)描述的意思是一個固定長度的字符串,有6個元素。一些lisp實現可能返回的僅僅是simple-string或者string或者(vector string-char)之類的信息。字符串(strings)和向量(vector)之間的關系我們會在第十三章解釋。
12.3 定義結構體
結構體是由任意數量的具名組件組成的程序員自定義lisp對象。結構體類型自動成為lisp類型繼承體系的一部分。宏defstruct定義結構體和制定名稱還有設定組件的默認值。例如,我們可以定義一個叫做starship的結構體:
defstruct語句定義了一個新的類型的額對象叫做starship,它的組件名是name,speed,condition,和shields。starship成為類型繼承系統的一部分并且可以被函數typep和type-of引用。
宏函數defstruct也做了一些其他的事情,他定義了一個佳作make-starship的后遭函數來創造新的這個類型的結構體。當一個新的starship被創造出來的時候,具名組件將會默認為nil,speed是0,condition是green,shields回事down。
標記#s是在common lisp中表示結構體的標準方式。在#S標記之后的列表包含結構體的類型的可變組件的名字和值。不要誤解了#S標記的括號表達式。對普通列表操作的函數讀結構體是不管用的。
雖然一般來說實例是由調用的構造函數創造出來的,但是使用#S標記,直接在頂層循環輸入starship對象也是可以的。請注意結構體必須加上引號,以避免被求值。
12.4 針對結構體的type斷言
defstruct的另一個副作用就是為結構體創造了一個類型斷言。基于結構體的名字,斷言叫做starship-p。
既然類型名字starship已經完全被整合近了類型系統中,那么他就可以被應用在斷言typep中和函數type-of中。
12.5 訪問和修改結構體
一個新的結構體被定義的時候,defstruct也為每一個組件創造了訪問函數,例如,他創造了一個starship-speed訪問函數來取回starship的組件speed。
訪問函數也可以用在setf替換相應位置描述上,也可以用在一般的賦值操作中。
使用訪問函數,我們就可以方便的定義我們自己的函數來操作結構體了。比如,下面的函數alert可以更改starship的shield和condition。
一個有經驗的lisp程序員或許會傾向于使用一個比X更具描述性的參數名。既然alert需要一個starship類型的輸入,那為什么直接使用這個名字作為參數名呢?
換個角度看,也有一些程序猿覺得這樣寫會讓人很困惑,因為starship及時一個本地變量名,又是一個類型名。如果你也覺得這樣和難理解,你也許會喜歡用一個縮寫的變量名,比如strship。
12.6 構造函數的關鍵字參數
在新的結構體實例被創造的時候,并不是一定要使用組件的默認值。我們可以在構造函數被調用的時候通過關鍵字函數來定義不同的值。,下面是一個使用make-starship構造函數的例子。
12.7 改變結構體定義
如果你想使用defstruct重新定義一個結構體類型,改變組件的名字或者順序的話,你應該吧就的結構體的類型整個丟掉,訪問函數也不會再起作用,也可能會出現其他問題。例如,已經存儲在starship類型的s3中的名字Reliant,如果我們重新定義starship,那么s3的值會成為一個古怪的對象,所有的域都會攪在一起。
為了修正這個問題,我們需要重定義構造函數make-starship,然后重構結構體。
小結
Common Lisp包括很多內建數據類型,本書只討論一些基礎的類型。Common Lisp類型系統都是可見的而且可擴展的。用戶可以通過自定義新的數據類型來擴展類型系統。
defstruct定義結構體類型,結構體定義包括所有組件的名字,還有可選的默認值的定義。如果沒有定義默認值的話,nil回座位默認值,defstruct也會為類型自動定義一個構造函數(makestarship),一集類型斷言(starship-p)。
本章涉及函數
結構體定義宏: DEFSTRUCT.
類型系統函數: TYPEP and TYPE-OF.
Lisp Toolkit: DESCRIBE and INSPECT
describe是一個接受任何類型lisp對象作為輸入的函數,然后打印這個類型的描述性信息。很多lisp系統的在線文檔都是以這種方式提供的。describe也是一個很好的方式來看Lisp系統的內部工作方式,因為你可以用來看像cons,nil,defun等的描述來學習一些有趣的東西。
由describe生成的準確輸出根據lisp實現的不同而不同,這里是一些典型的例子。作為一個lisp初學者你也許不明白上面的全部信息,但是使用幫助手冊(用describe也可以)來排解困惑也是一件很有意思的事情。
describe在展現結構體上是特別有用的。在大部分Common Lisp實現中,describe以一種比#S標記更具可讀性的方式來展現結構體的域。
另一個值得一試的工具是inspect,如果你的計算機有一個鼠標和一個窗口系統的話,inspect或許可以使你用鼠標指向來檢查對象的組件。嘗試定義一個像half那樣的簡單哈數,然后用表達式(inspect ‘half)來看看函數定義在內部是怎么存儲的。
不同的lisp實現提供不同的inspect程序,你會需要看看你使用的特定lisp手冊來學習一下使用inspect。
第十二章進階話題
12.8 打印結構體的函數
發明一種專門用來打印結構體的標記是很方便的。例如,在打印結構體的時候,我們也許不想要看所有對象中的域。只是想看名字罷了。在Common lisp中,打印縮略結構描述的慣例是編造一個標記,由“#<”開始,由“>”結束,中間包括了結構體類型和任何需要的定義信息。
寫我們自己的打印函數,第一步是定制starship對象的打印方式,。他必須接受三個輸入:將要被打印的對象,打印的流向,還有一個數字(叫做深度),也就是Common lisp用來限制打印的深度,控制復雜結構的打印粒度。我們會在本書中忽視深度參數,但是我們的函數必須接受三個參數來正常工作。
我們來測試一下,調用這個函數,用starship作為第一個輸入,用T作為第二個輸入(T指向默認打印輸出流,也就是控制臺),還有深度,0。
現在我們來把這個函數包括近defstruct的一個選項里。
當一個結構體包含其他結構體作為組件或者我們想要限制大部分信息的時候,打印函數是特別有用的。當一個結構體重出現循環指針的時候,大隱函數幾乎是白哦準配置。舉個例子,每一艘船都有一個船長,每一個船長都有一艘船。如果結構體中船長和船的指針互相指向的話,打印其中一個就會產生無限循環,或者被迫使用那個特別沒有美感的#1#標記來標識循環指針結構。但是如果打印函數只是打印名字字段的話,那么循環指針的問題也就沒有那么突出了,打印的方式也比較優雅了。
12.9 結構體的相等
函數equal不會認為兩個結構體相等,及時他們擁有相同的字段。
然而,eqaulp函數就會認為他們是相等的,如果他們的組件類型是一樣的而且值也是相同的話。
equalp也是不同于equal的,在比較兩個字符串的時候會忽略大小寫。
12.10 從其他結構體繼承
結構體類型可以使用defstruct的選項:include來被組織進一個層次體系里面。例如,我們可以定義一個結構體類型ship,它的組件是name,captain還有crew-size。之后我們可以定義starship作為一種類型的ship,加上組件weapons和shields,還有supply-ship作為一種ship的附加類型,加上cargo。
starship結構體包含了ship結構體的所有組件。因此,當我們創造一個starship,它的首先三個組件就是name,captainhecrew-size。supply-ship類型也是一樣。
Enterprise同事是一個ship類型和starship類型,所以對于兩個類型的斷言,他都會返回true。
最后,請注意,ship的訪問函數對于starship和supply-ship這些子類型也是管用的。因此我們可以使用ship-captain函數或者starshipcaptain函數訪問Enterprise的captain字段,但是supply-ship-captain不可以使用。