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可以被指定。

你可能感兴趣的:(Lisp,Racket编程指南(中文译),Racket)