天天看點

Racket程式設計指南——13 類和對象

13 類和對象

一個類(class)表達式表示一類值,就像一個lambda表達式一樣:

(class superclass-expr decl-or-expr ...)

superclass-expr确定為新類的基類。每個 decl-or-expr既是一個聲明,關系到對方法、字段和初始化參數,也是一個表達式,每次求值就執行個體化類。換句話說,與方法之類的構造器不同,類具有與字段和方法聲明交錯的初始化表達式。

按照慣例,類名以%結束。内置根類是object%。下面的表達式用公共方法get-size、grow和eat建立一個類:

(class object%
  (init size)                ; 初始化參數
  (define current-size size) ; 字段
  (super-new)                ; 基類初始化
  (define/public (get-size)
    current-size)
  (define/public (grow amt)
    (set! current-size (+ amt current-size)))
  (define/public (eat other-fish)
    (grow (send other-fish get-size))))

當通過new表執行個體化類時,size的初始化參數必須通過一個命名參數提供:

(new (class object% (init size) ....) [size 10])

當然,我們還可以命名類及其執行個體:

(define fish% (class object% (init size) ....))
(define charlie (new fish% [size 10]))

在fish%的定義中,current-size是一個以size值初始化參數開頭的私有字段。像size這樣的初始化參數隻有在類執行個體化時才可用,是以不能直接從方法引用它們。與此相反,current-size字段可用于方法。

在class中的(super-new)表達式調用基類的初始化。在這種情況下,基類是object%,它沒有帶初始化參數也沒有執行任何工作;必須使用super-new,因為一個類總必須總是調用其基類的初始化。

初始化參數、字段聲明和表達式如(super-new)可以以類(class)中的任何順序出現,并且它們可以與方法聲明交織在一起。類中表達式的相對順序決定了執行個體化過程中的求值順序。例如,如果一個字段的初始值需要調用一個方法,它隻有在基類初始化後才能工作,然後字段聲明必須放在super-new調用後。以這種方式排序字段和初始化聲明有助于規避不可避免的求值。方法聲明的相對順序對求值沒有影響,因為方法在類執行個體化之前被完全定義。

13.1 方法

fish%中的三個define/public聲明都引入了一種新方法。聲明使用與Racket函數相同的文法,但方法不能作為獨立函數通路。調用fish%對象的grow方法需要send表:

> (send charlie grow 6)
> (send charlie get-size)
16

在fish%中,自方法可以被像函數那樣調用,因為方法名在作用域中。例如,fish%中的eat方法直接調用grow方法。在類中,試圖以除方法調用以外的任何方式使用方法名會導緻文法錯誤。

在某些情況下,一個類必須調用由基類提供但不能被重寫的方法。在這種情況下,類可以使用帶this的send來通路該方法:

(define hungry-fish% (class fish% (super-new)
                       (define/public (eat-more fish1 fish2)
                         (send this eat fish1)
                         (send this eat fish2))))

另外,類可以聲明一個方法使用inherit(繼承)的存在,該方法将方法名引入到直接調用的作用域中:

(define hungry-fish% (class fish% (super-new)
                       (inherit eat)
                       (define/public (eat-more fish1 fish2)
                         (eat fish1) (eat fish2))))

在inherit聲明中,如果fish%沒有提供一個eat方法,那麼在對 hungry-fish%類表的求值中會出現一個錯誤。與此相反,用(send this ....),直到eat-more方法被調和send表被求值前不會發出錯誤信号。是以,inherit是首選。

send的另一個缺點是它比inherit效率低。一個方法的請求通過send調用尋找在運作時在目标對象的類的方法,使send類似于java方法調用接口。相反,基于inherit的方法調用使用一個類的方法表中的偏移量,它在類建立時計算。

為了在從方法類之外調用方法時實作與繼承方法調用類似的性能,程式員必須使用generic(泛型)表,它生成一個特定類和特定方法的generic方法,用send-generic調用:

(define get-fish-size (generic fish% get-size))
> (send-generic charlie get-fish-size)
16
> (send-generic (new hungry-fish% [size 32]) get-fish-size)
32
> (send-generic (new object%) get-fish-size)
generic:get-size: target is not an instance of the generic's
class
  target: (object)
  class name: fish%

粗略地說,表單将類和外部方法名轉換為類方法表中的位置。如上一個例子所示,通過泛型方法發送檢查它的參數是泛型類的一個執行個體。

是否在class内直接調用方法,通過泛型方法,或通過send,方法以通常的方式重寫工程:

(define picky-fish% (class fish% (super-new)
                      (define/override (grow amt)
                        (super grow (* 3/4 amt)))))
(define daisy (new picky-fish% [size 20]))
> (send daisy eat charlie)
> (send daisy get-size)
32

在picky-fish%的grow方法是用define/override聲明的,而不是 define/public,因為grow是作為一個重寫的申明的意義。如果grow已經用define/public聲明,那麼在對類表達式求值時會發出一個錯誤,因為fish%已經提供了grow。

使用define/override也允許通過super調用調用重寫的方法。例如,grow在picky-fish%實作使用super代理給基類的實作。

13.2 初始化參數

因為picky-fish%申明沒有任何初始化參數,任何初始化值在(new picky-fish% ....)裡提供都被傳遞給基類的初始化,即傳遞給fish%。子類可以在super-new調用其基類時提供額外的初始化參數,這樣的初始化參數會優先于參數提供給new。例如,下面的size-10-fish%類總是産生大小為10的魚:

(define size-10-fish% (class fish% (super-new [size 10])))
> (send (new size-10-fish%) get-size)
10

就size-10-fish%來說,用new提供一個size初始化參數會導緻初始化錯誤;因為在super-new裡的size優先,size提供給new沒有目标申明。

如果class表聲明一個預設值,則初始化參數是可選的。例如,下面的default-10-fish%類接受一個size的初始化參數,但如果在執行個體裡沒有提供值那它的預設值是10:

(define default-10-fish% (class fish%
                           (init [size 10])
                           (super-new [size size])))
> (new default-10-fish%)
(object:default-10-fish% ...)
> (new default-10-fish% [size 20])
(object:default-10-fish% ...)

在這個例子中,super-new調用傳遞它自己的size值作為size初始化初始化參數傳遞給基類。

13.3 内部和外部名稱

在default-10-fish%中size的兩個使用揭示了類成員辨別符的雙重身份。當size是new或super-new中的一個括号對的第一辨別符,size是一個外部名稱(external name),象征性地比對到類中的初始化參數。當size作為一個表達式出現在default-10-fish%中,size是一個内部名稱(internal name),它是詞法作用域。類似地,對繼承的eat方法的調用使用eat作為内部名稱,而一個eat的send的使用作為一個外部名稱。

class表的完整文法允許程式員為類成員指定不同的内部和外部名稱。由于内部名稱是本地的,是以可以重命名它們,以避免覆寫或沖突。這樣的改名不總是必要的,但重命名缺乏的解決方法可以是特别繁瑣。

13.4 接口(Interface)

接口對于檢查一個對象或一個類實作一組具有特定(隐含)行為的方法非常有用。接口的這種使用有幫助的,即使沒有靜态類型系統(那是java有接口的主要原因)。

Racket中的接口通過使用interface表建立,它隻聲明需要去實作的接口的方法名稱。接口可以擴充其它接口,這意味着接口的實作會自動實作擴充接口。

(interface (superinterface-expr ...) id ...)

為了聲明一個實作一個接口的類,必須使用class*表代替class:

(class* superclass-expr (interface-expr ...) decl-or-expr ...)

例如,我們不必強制所有的fish%類都是源自于fish%,我們可以定義fish-interface并改變fish%類來聲明它實作了fish-interface:

(define fish-interface (interface () get-size grow eat))
(define fish% (class* object% (fish-interface) ....))

如果fish%的定義不包括get-size、grow和eat方法,那麼在class*表求值時會出現錯誤,因為實作fish-interface接口需要這些方法。

is-a?判斷接受一個對象作為它的第一個參數,同時類或接口作為它的第二個參數。當給了一個類,無論對象是該類的執行個體或者派生類的執行個體,is-a?都執行檢查。當給一個接口,無論對象的類是否實作接口,is-a?都執行檢查。另外,implementation?判斷檢查給定類是否實作給定接口。

13.5 Final、Augment和Inner

在java中,一個class表的方法可以被指定為最終的(final),這意味着一個子類不能重寫方法。一個最終方法是使用public-final或override-final申明,取決于聲明是為一個新方法還是一個重寫實作。

在允許與不允許任意完全重寫的兩個極端之間,類系統還支援Beta類型的可擴充(augmentable)方法。一個帶pubment聲明的方法類似于public,但方法不能在子類中重寫;它僅僅是可擴充。一個pubment方法必須顯式地使用inner調用一個擴充(如果有);一個子類使用pubment擴充方法,而不是使用override。

一般來說,一個方法可以在類派生的擴充模式和重寫模式之間進行切換。augride方法詳述表明了一個擴充,這裡這個擴充本身在子類中是可重寫的的方法(雖然這個基類的實作不能重寫)。同樣,overment重寫一個方法并使得重寫的實作變得可擴充。

13.6 控制外部名稱的範圍

正如《内部和外部名稱》(Internal and External Names)所指出的,類成員既有内部名稱,也有外部名稱。成員定義在本地綁定内部名稱,此綁定可以在本地重命名。與此相反,外部名稱預設情況下具有全局範圍,成員定義不綁定外部名稱。相反,成員定義指的是外部名稱的現有綁定,其中成員名綁定到成員鍵(member key);一個類最終将成員鍵映射到方法、字段和初始化參數。

回頭看hungry-fish%類(class)表達式:

(define hungry-fish% (class fish% ....
                       (inherit eat)
                       (define/public (eat-more fish1 fish2)
                         (eat fish1) (eat fish2))))

在求值過程中hungry-fish%類和fish%類指相同的eat的全局綁定。在運作時,在hungry-fish%中調用eat是通過共享綁定到eat的方法鍵和fish%中的eat方法相比對。

對外部名稱的預設綁定是全局的,但程式員可以用define-member-name表引入外部名稱綁定。

(define-member-name id member-key-expr)

特别是,通過使用(generate-member-key)作為member-key-expr,外部名稱可以為一個特定的範圍局部化,因為生成的成員鍵範圍之外的通路。換句話說,define-member-name給外部名稱一種私有包範圍,但從包中概括為Racket中的任意綁定範圍。

例如,下面的fish%類和pond%類通過一個get-depth方法配合,隻有這個配合類可以通路:

(define-values (fish% pond%) ; 兩個互相遞歸類
  (let ()
    (define-member-name get-depth (generate-member-key))
    (define fish%
      (class ....
        (define my-depth ....)
        (define my-pond ....)
        (define/public (dive amt)
        (set! my-depth
              (min (+ my-depth amt)
                   (send my-pond get-depth))))))
    (define pond%
      (class ....
        (define current-depth ....)
        (define/public (get-depth) current-depth)))
    (values fish% pond%)))

外部名稱在名稱空間中,将它們與其它Racket名稱分隔開。這個單獨的命名空間被隐式地用于send中的方法名、在new中的初始化參數名稱,或成員定義中的外部名稱。特殊表 member-name-key提供對任意表達式位置外部名稱的綁定的通路:(member-name-key id)在目前範圍内生成id的成員鍵綁定。

成員鍵值主要用于define-member-name表。通常,(member-name-key id)捕獲id的方法鍵,以便它可以在不同的範圍内傳遞到define-member-name的使用。這種能力證明推廣混合是有用的,作為接下來的讨論。

13.7 混合(mixin)

因為class(類)是一種表達表,而不是如同在Smalltalk和java裡的一個頂級的聲明,一個class表可以嵌套在任何詞法範圍内,包括lambda(λ)。其結果是一個混合(mixin),即,一個類的擴充,是相對于它的基類的參數化。

例如,我們可以參數化picky-fish%類來覆寫它的基類進而定義picky-mixin:

(define (picky-mixin %)
  (class % (super-new)
    (define/override (grow amt) (super grow (* 3/4 amt)))))
(define picky-fish% (picky-mixin fish%))

Smalltalk風格類和Racket類之間的許多小的差異有助于混合的有效利用。特别是,define/override的使用使得picky-mixin期望一個類帶有一個grow方法更明确。如果picky-mixin應用于一個沒有grow方法的類,一旦應用picky-mixin則會發出一個錯誤的資訊。

同樣,當應用混合時使用inherit(繼承)執行“方法存在(method existence)”的要求:

(define (hungry-mixin %)
  (class % (super-new)
    (inherit eat)
    (define/public (eat-more fish1 fish2)
      (eat fish1)
      (eat fish2))))

mixin的優勢是,我們可以很容易地将它們結合起來以建立新的類,其共享的實作不适合一個繼承層次——沒有多繼承相關的歧義。配備picky-mixin和hungry-mixin,為“hungry”創造了一個類,但“picky fish”是直截了當的:

(define picky-hungry-fish%
  (hungry-mixin (picky-mixin fish%)))

關鍵詞初始化參數的使用是混合的易于使用的重點。例如,picky-mixin和hungry-mixin可以通過合适的eat方法和grow方法增加任何類,因為它們在它們的super-new表達式裡沒有指定初始化參數也沒有添加東西:

(define person%
  (class object%
    (init name age)
    ....
    (define/public (eat food) ....)
    (define/public (grow amt) ....)))
(define child% (hungry-mixin (picky-mixin person%)))
(define oliver (new child% [name "Oliver"] [age 6]))

最後,對類成員的外部名稱的使用(而不是詞法作用域辨別符)使得混合使用很友善。添加picky-mixin到person%運作,因為這個名字eat和grow比對,在fish%和person%裡沒有任何eat和grow的優先申明可以是同樣的方法。當成員名稱意外碰撞後,此特性是一個潛在的缺陷;一些意外沖突可以通過限制外部名稱作用域來糾正,就像在《控制外部名稱的範圍(Controlling the Scope of External Names)》所讨論的那樣。

13.7.1 混合和接口

使用implementation?,picky-mixin可以要求其基類實作grower-interface,這可以是由fish%和person%實作:

(define grower-interface (interface () grow))
(define (picky-mixin %)
  (unless (implementation? % grower-interface)
    (error "picky-mixin: not a grower-interface class"))
  (class % ....))

另一個使用帶混合的接口是标記類通過混合産生,是以,混合執行個體可以被識别。換句話說,is-a?不能在一個混合上展現為一個函數運作,但它可以識别為一個接口(有點像一個特定的接口),它總是被混合所實作。例如,通過picky-mixin生成的類可以被picky-interface所标記,使是is-picky?去判定:

(define picky-interface (interface ()))
(define (picky-mixin %)
  (unless (implementation? % grower-interface)
    (error "picky-mixin: not a grower-interface class"))
  (class* % (picky-interface) ....))
(define (is-picky? o)
  (is-a? o picky-interface))
13.7.2 The mixin表

為執行混合而編纂lambda加class模式,包括對混合的定義域和值域接口的使用,類系統提供了一個mixin宏:

(mixin (interface-expr ...) (interface-expr ...)
  decl-or-expr ...)

interface-expr的第一個集合确定混合的定義域,第二個集合确定值域。就是說,擴張是一個函數,它測試是否一個給定的基類實作interface-expr的第一個序列,并産生一個類實作interface-expr的第二個序列。其它要求,如在基類的繼承方法的存在,然後檢查mixin表的class擴充。例如:

> (define choosy-interface (interface () choose?))
> (define hungry-interface (interface () eat))
> (define choosy-eater-mixin
    (mixin (choosy-interface) (hungry-interface)
      (inherit choose?)
      (super-new)
      (define/public (eat x)
        (cond
          [(choose? x)
           (printf "chomp chomp chomp on ~a.\n" x)]
          [else
           (printf "I'm not crazy about ~a.\n" x)]))))
> (define herring-lover%
    (class* object% (choosy-interface)
      (super-new)
      (define/public (choose? x)
        (regexp-match #px"^herring" x))))
> (define herring-eater% (choosy-eater-mixin herring-lover%))
> (define eater (new herring-eater%))
> (send eater eat "elderberry")
I'm not crazy about elderberry.
> (send eater eat "herring")
chomp chomp chomp on herring.
> (send eater eat "herring ice cream")
chomp chomp chomp on herring ice cream.

混合不僅覆寫方法,并引入公共方法,它們也可以擴充方法,引入擴充的方法,添加一個可重寫的擴充,并添加一個可擴充的覆寫——所有這些事一個類都能完成(參見《Final、Augment和Inner》部分)。

13.7.3 參數化的混合

正如在《控制外部名稱的範圍》(Controlling the Scope of External Names)中指出的,外部名稱可以用define-member-name綁定。這個工具允許一個混合用定義或使用的方法概括。例如,我們可以通過對eat的外部成員鍵的使用參數化hungry-mixin:

(define (make-hungry-mixin eat-method-key)
  (define-member-name eat eat-method-key)
  (mixin () () (super-new)
    (inherit eat)
    (define/public (eat-more x y) (eat x) (eat y))))

獲得一個特定的hungry-mixin,我們必須應用這個函數到一個成員鍵,它指向一個适當的eat方法,我們可以獲得 member-name-key的使用:

((make-hungry-mixin (member-name-key eat))
 (class object% .... (define/public (eat x) 'yum)))

以上,我們應用hungry-mixin給一個匿名類,它提供eat,但我們也可以把它和一個提供chomp的類組合,相反:

((make-hungry-mixin (member-name-key chomp))
 (class object% .... (define/public (chomp x) 'yum)))

13.8 特征(trait)

一個特征(trait)類似于一個mixin,它封裝了一組方法添加到一個類裡。一個特征不同于一個mixin,它自己的方法是可以用特征運算符操控的,比如trait-sum(合并這兩個特征的方法)、trait-exclude(從一個特征中移除方法)以及trait-alias(添加一個帶有新名字的方法的拷貝;它不重定向到對任何舊名字的調用)。

混合和特征之間的實際差别是兩個特征可以組合,即使它們包括了共有的方法,而且即使兩者的方法都可以合理地覆寫其它方法。在這種情況下,程式員必須明确地解決沖突,通常通過混淆方法,排除方法,以及合并使用别名的新特性。

假設我們的fish%程式員想要定義兩個類擴充,spots和stripes,每個都包含get-color方法。fish的spot不應該覆寫的stripe,反之亦然;相反,一個spots+stripes-fish%應結合兩種顔色,這是不可能的如果spots和stripes是普通混合實作。然而,如果spots和stripes作為特征來實作,它們可以組合在一起。首先,我們在每個特征中給get-color起一個别名為一個不沖突的名稱。第二,get-color方法從兩者中移除,隻有别名的特征被合并。最後,新特征用于建立一個類,它基于這兩個别名引入自己的get-color方法,生成所需的spots+stripes擴充。

13.8.1 特征作為混合集

在Racket裡實作特征的一個自然的方法是如同一組混合,每個特征方法帶一個mixin。例如,我們可以嘗試如下定義spots和stripes的特征,使用關聯清單來表示集合:

(define spots-trait
  (list (cons 'get-color
               (lambda (%) (class % (super-new)
                             (define/public (get-color)
                               'black))))))
(define stripes-trait
  (list (cons 'get-color
              (lambda (%) (class % (super-new)
                            (define/public (get-color)
                              'red))))))

一個集合的表示,如上面所述,允許trait-sum和trait-exclude做為簡單操作;不幸的是,它不支援trait-alias運算符。雖然一個混合可以在關聯表裡複制,混合有一個固定的方法名稱,例如,get-color,而且混合不支援方法重命名操作。支援trait-alias,我們必須在擴充方法名上參數化混合,同樣地eat在參數化混合(參數化的混合)中進行參數化。

為了支援trait-alias操作,spots-trait應表示為:

(define spots-trait
  (list (cons (member-name-key get-color)
              (lambda (get-color-key %)
                (define-member-name get-color get-color-key)
                (class % (super-new)
                  (define/public (get-color) 'black))))))

當spots-trait中的get-color方法是給get-trait-color的别名并且get-color方法被去除,由此産生的特性如下:

(list (cons (member-name-key get-trait-color)
            (lambda (get-color-key %)
              (define-member-name get-color get-color-key)
              (class % (super-new)
                (define/public (get-color) 'black)))))

應用特征T到一個類C和獲得一個派生類,我們用((trait->mixin T) C)。trait->mixin函數用給混合的方法和部分 C擴充的鍵提供每個T的混合:

(define ((trait->mixin T) C)
  (foldr (lambda (m %) ((cdr m) (car m) %)) C T))

是以,當上述特性與其它特性結合,然後應用到類中時,get-color的使用将成為外部名稱get-trait-color的引用。

13.8.2 特征的繼承與基類

特性的這個第一個實作支援trait-alias,它支援一個調用自身的特性方法,但是它不支援調用彼此的特征方法。特别是,假設一個spot-fish的市場價值取決于它的斑點顔色:

(define spots-trait
  (list (cons (member-name-key get-color) ....)
        (cons (member-name-key get-price)
              (lambda (get-price %) ....
                (class % ....
                  (define/public (get-price)
                    .... (get-color) ....))))))

在這種情況下,spots-trait的定義失敗,因為get-color是不在get-price混合範圍之内。事實上,當特征應用于一個類時依賴于混合程式的順序,當get-price混合應用于類時get-color方法可能不可獲得。是以添加一個(inherit get-color)申明給get-price混合并不解決問題。

一種解決方案是要求在像get-price方法中使用(send this get-color)。這種更改是有效的,因為send總是延遲方法查找,直到對方法的調用被求值。然而,延遲查找比直接調用更為昂貴。更糟糕的是,它也延遲檢查get-color方法是否存在。

第二個,實際上,并且有效的解決方案是改變特征編碼。具體來說,我們代表每個方法作為一對混合:一個引入方法,另一個實作它。當一個特征應用于一個類,所有的引入方法混合首先被應用。然後實作方法混合可以使用inherit去直接通路任何引入的方法。

(define spots-trait
  (list (list (local-member-name-key get-color)
              (lambda (get-color get-price %) ....
                (class % ....
                  (define/public (get-color) (void))))
              (lambda (get-color get-price %) ....
                (class % ....
                  (define/override (get-color) 'black))))
        (list (local-member-name-key get-price)
              (lambda (get-price get-color %) ....
                (class % ....
                  (define/public (get-price) (void))))
              (lambda (get-color get-price %) ....
                (class % ....
                  (inherit get-color)
                  (define/override (get-price)
                    .... (get-color) ....))))))

有了這個特性編碼, trait-alias添加一個帶新名稱的新方法,但它不會改變對舊方法的任何引用。

13.8.3 trait(特征)表

通用特性模式顯然對程式員直接使用來說太複雜了,但很容易在trait宏中編譯:

(trait trait-clause ...)

在可選項的inherit(繼承)從句中的id對expr方法中的直接引用是有效的,并且它們必須提供其它特征或者基類,其特征被最終應用。

使用這個表結合特征操作符,如trait-sum、trait-exclude、trait-alias和trait->mixin,我們可以實作spots-trait和stripes-trait作為所需。

(define spots-trait
  (trait
    (define/public (get-color) 'black)
    (define/public (get-price) ... (get-color) ...)))
(define stripes-trait
  (trait
    (define/public (get-color) 'red)))
(define spots+stripes-trait
  (trait-sum
   (trait-exclude (trait-alias spots-trait
                               get-color get-spots-color)
                  get-color)
   (trait-exclude (trait-alias stripes-trait
                               get-color get-stripes-color)
                  get-color)
   (trait
     (inherit get-spots-color get-stripes-color)
     (define/public (get-color)
       .... (get-spots-color) .... (get-stripes-color) ....))))

13.9 類合約

由于類是值,它們可以跨越合約邊界,我們可能希望用合約保護給定類的一部分。為此,使用class/c表。class/c表具有許多子表,其描述關于字段和方法兩種類型的合約:有些通過執行個體化對象影響使用,有些影響子類。

13.9.1 外部類合約

在最簡單的表中,class/c保護從合約類執行個體化的對象的公共字段和方法。還有一種object/c表,可用于類似地保護特定對象的公共字段和方法。擷取animal%的以下定義,它使用公共字段作為其size屬性:

(define animal%
  (class object%
    (super-new)
    (field [size 10])
    (define/public (eat food)
      (set! size (+ size (get-field size food))))))

對于任何執行個體化的animal%,通路size字段應該傳回一個正數。另外,如果設定了size字段,則應該配置設定一個正數。最後,eat方法應該接收一個參數,它是一個包含一個正數的size字段的對象。為了確定這些條件,我們将用适當的合約定義animal%類:

(define positive/c (and/c number? positive?))
(define edible/c (object/c (field [size positive/c])))
(define/contract animal%
  (class/c (field [size positive/c])
           [eat (->m edible/c void?)])
  (class object%
    (super-new)
    (field [size 10])
    (define/public (eat food)
      (set! size (+ size (get-field size food))))))

這裡我們使用->m來描述eat的行為,因為我們不需要描述這個this參數的任何要求。既然我們有我們的合約類,就可以看出對size和eat的合約都是強制執行的:

> (define bob (new animal%))
> (set-field! size bob 3)
> (get-field size bob)
3
> (set-field! size bob 'large)
animal%: contract violation
  expected: positive/c
  given: 'large
  in: the size field in
      (class/c
       (eat
        (->m
         (object/c (field (size positive/c)))
         void?))
       (field (size positive/c)))
  contract from: (definition animal%)
  blaming: top-level
   (assuming the contract is correct)
  at: eval:31.0
> (define richie (new animal%))
> (send bob eat richie)
> (get-field size bob)
13
> (define rock (new object%))
> (send bob eat rock)
eat: contract violation;
 no public field size
  in: the 1st argument of
      the eat method in
      (class/c
       (eat
        (->m
         (object/c (field (size positive/c)))
         void?))
       (field (size positive/c)))
  contract from: (definition animal%)
  contract on: animal%
  blaming: top-level
   (assuming the contract is correct)
  at: eval:31.0
> (define giant (new (class object% (super-new) (field [size 'large]))))
> (send bob eat giant)
eat: contract violation
  expected: positive/c
  given: 'large
  in: the size field in
      the 1st argument of
      the eat method in
      (class/c
       (eat
        (->m
         (object/c (field (size positive/c)))
         void?))
       (field (size positive/c)))
  contract from: (definition animal%)
  contract on: animal%
  blaming: top-level
   (assuming the contract is correct)
  at: eval:31.0

對于外部類合同有兩個重要的警告。首先,當動态分派的目标是合約類的方法實施時,隻有在合同邊界内才實施外部方法合同。重寫該實作,進而改變動态分派的目标,将意味着不再為客戶機強制執行該合約,因為通路該方法不再越過合約邊界。與外部方法合約不同,外部字段合約對于子類的客戶機總是強制執行,因為字段不能被覆寫或屏蔽。

第二,這些合約不以任何方式限制animal%的子類。被子類繼承和使用的字段和方法不被這些合約檢查,并且通過super對基類方法的使用也不檢查。下面的示例說明了兩個警告:

(define large-animal%
  (class animal%
    (super-new)
    (inherit-field size)
    (set! size 'large)
    (define/override (eat food)
      (display "Nom nom nom") (newline))))
> (define elephant (new large-animal%))
> (send elephant eat (new object%))
Nom nom nom
> (get-field size elephant)
animal%: broke its own contract
  promised: positive/c
  produced: 'large
  in: the size field in
      (class/c
       (eat
        (->m
         (object/c (field (size positive/c)))
         void?))
       (field (size positive/c)))
  contract from: (definition animal%)
  blaming: (definition animal%)
   (assuming the contract is correct)
  at: eval:31.0
13.9.2 内部類合約

注意,從elephant對象檢索size字段歸咎于animal%違反合約。這種歸咎是正确的,但對animal%類來說是不公平的,因為我們還沒有提供一種保護自己免受子類攻擊的方法。為此我們添加内部類合約,它提供指令給子類以指明它們如何通路和重寫基類的特征。外部類和内部類合約之間的差別在于是否允許類層次結構中較弱的合約,其不變性可能被子類内部破壞,但應通過執行個體化的對象強制用于外部使用。

作為可用的保護類型的簡單示例,我們提供了一個針對animal%類的示例,它使用所有适用的表:

(class/c (field [size positive/c])
         (inherit-field [size positive/c])
         [eat (->m edible/c void?)]
         (inherit [eat (->m edible/c void?)])
         (super [eat (->m edible/c void?)])
         (override [eat (->m edible/c void?)]))

這個類合約不僅確定animal%類的對象像以前一樣受到保護,而且確定animal%類的子類隻在size字段中存儲适當的值,并适當地使用animal%的size實作。這些合約表隻影響類層次結構中的使用,并且隻影響跨合約邊界的方法調用。

這意味着,inherit(繼承)隻會影響到一個方法的子類使用直到子類重寫方法,而override隻影響從基類進入方法的子類的重寫實作。由于這些僅影響内部使用,是以在使用這些類的對象時,override表不會自動将子類插入到義務(obligations)中。此外,使用override僅是說得通,是以隻能用于沒有beta樣式增強的方法。下面的示例顯示了這種差異:

(define/contract sloppy-eater%
  (class/c [eat (->m edible/c edible/c)])
  (begin
    (define/contract glutton%
      (class/c (override [eat (->m edible/c void?)]))
      (class animal%
        (super-new)
        (inherit eat)
        (define/public (gulp food-list)
          (for ([f food-list])
            (eat f)))))
    (class glutton%
      (super-new)
      (inherit-field size)
      (define/override (eat f)
        (let ([food-size (get-field size f)])
          (set! size (/ food-size 2))
          (set-field! size f (/ food-size 2))
          f)))))
> (define pig (new sloppy-eater%))
> (define slop1 (new animal%))
> (define slop2 (new animal%))
> (define slop3 (new animal%))
> (send pig eat slop1)
(object:animal% ...)
> (get-field size slop1)
5
> (send pig gulp (list slop1 slop2 slop3))
eat: broke its own contract
  promised: void?
  produced: (object:animal% ...)
  in: the range of
      the eat method in
      (class/c
       (override (eat
                  (->m
                   (object/c
                    (field (size positive/c)))
                   void?))))
  contract from: (definition glutton%)
  contract on: glutton%
  blaming: (definition sloppy-eater%)
   (assuming the contract is correct)
  at: eval:47.0

除了這裡的内部類合約表所顯示的之外,這裡有beta樣式可擴充的方法類似的表。inner表描述了這個子類,它被要求從一個給定的方法擴充。augment和augride告訴子類,該給定的方法是一種被增強的方法,并且對子類方法的任何調用将動态配置設定到基類中相應的實作。這樣的調用将根據給定的合約進行檢查。這兩種表的差別在于augment的使用意味着子類可以增強給定的方法,而augride的使用表示子類必須反而重寫目前增強。

這意味着并不是所有的表都可以同時使用。隻有override、augment和augride中的一個表可用于一個給定的方法,而如果給定的方法已經完成,這些表沒有一個可以使用。此外, 僅在augride或override可以指定時,super可以被指定為一個給定的方法。同樣,隻有augment或augride可以指定時,inner可以被指定。

繼續閱讀