Racket編程指南——13 類和對象

13 類和對象

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

(class superclass-expr decl-or-expr ...)
superclass-expr確定爲新類的基類。每個decl-or-expr既是一個聲明,關係到對方法、字段和初始化參數,也是一個表達式,每次求值就實例化類。換句話說,與方法之類的構造器不同,類具有與字段和方法聲明交錯的初始化表達式。

按照慣例,類名以%結束。內置根類是object%。下面的表達式用公共方法get-sizegroweat創建一個類:

(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方法。在類中,試圖以除方法調用以外的任何方式使用方法名會導致語法錯誤。

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

(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調用調用重寫的方法。例如,growpicky-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的兩個使用揭示了類成員標識符的雙重身份。當sizenewsuper-new中的一個括號對的第一標識符,size是一個外部名稱(external name),象徵性地匹配到類中的初始化參數。當size作爲一個表達式出現在default-10-fish%中,size是一個內部名稱(internal name),它是詞法作用域。類似地,對繼承的eat方法的調用使用eat作爲內部名稱,而一個eatsend的使用作爲一個外部名稱。

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-sizegroweat方法,那麼在class*表求值時會出現錯誤,因爲實現fish-interface接口需要這些方法。

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

13.5 Final、Augment和Inner

在java中,一個class表的方法可以被指定爲最終的(final),這意味着一個子類不能重寫方法。一個最終方法是使用public-finaloverride-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-mixinhungry-mixin,爲“hungry”創造了一個類,但“picky fish”是直截了當的:

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

關鍵詞初始化參數的使用是混合的易於使用的重點。例如,picky-mixinhungry-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-mixinperson%運行,因爲這個名字eatgrow匹配,在fish%person%裏沒有任何eatgrow的優先申明可以是同樣的方法。當成員名稱意外碰撞後,此特性是一個潛在的缺陷;一些意外衝突可以通過限制外部名稱作用域來糾正,就像在《控制外部名稱的範圍(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

爲執行混合而編纂lambdaclass模式,包括對混合的定義域和值域接口的使用,類系統提供了一個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%程序員想要定義兩個類擴展,spotsstripes,每個都包含get-color方法。fish的spot不應該覆蓋的stripe,反之亦然;相反,一個spots+stripes-fish%應結合兩種顏色,這是不可能的如果spotsstripes是普通混合實現。然而,如果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-sumtrait-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(繼承)從句中的idexpr方法中的直接引用是有效的,並且它們必須提供其它特徵或者基類,其特徵被最終應用。

使用這個表結合特徵操作符,如trait-sumtrait-excludetrait-aliastrait->mixin,我們可以實現spots-traitstripes-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參數的任何要求。既然我們有我們的合約類,就可以看出對sizeeat的合約都是強制執行的:

> (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表描述了這個子類,它被要求從一個給定的方法擴展。augmentaugride告訴子類,該給定的方法是一種被增強的方法,並且對子類方法的任何調用將動態分配到基類中相應的實現。這樣的調用將根據給定的合約進行檢查。這兩種表的區別在於augment的使用意味着子類可以增強給定的方法,而augride的使用表示子類必須反而重寫當前增強。

這意味着並不是所有的表都可以同時使用。只有overrideaugmentaugride中的一個表可用於一個給定的方法,而如果給定的方法已經完成,這些表沒有一個可以使用。此外, 僅在augrideoverride可以指定時,super可以被指定爲一個給定的方法。同樣,只有augmentaugride可以指定時,inner可以被指定。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章