On this page:
13.1 Methods
13.2 Initialization Arguments
13.3 Internal and External Names
13.4 Interfaces
13.5 Final, Augment, and Inner
13.6 Controlling the Scope of External Names
13.7 Interfaces
13.7.1 Mixins and Interfaces
13.7.2 The mixin Form
13.7.3 Parameterized Mixins
13.8 Interfaces
13.8.1 Traits as Sets of Mixins
13.8.2 Inherit and Super in Traits
13.8.3 The trait Form
13.9 Class Contracts
13.9.1 External Class Contracts
13.9.2 Internal Class Contracts

13 Classes and Objects🔗

本章基于论文 [Flatt06]。

(require racket/class) is needed for #lang racket/base.

class 表达式表示一个一等值, 就像 lambda 表达式一样:

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

superclass-expr 决定新类的超类。每个 decl-or-expr 要么是与方法、字段和初始化参数相关的声明, 要么是每次实例化类时求值的表达式。换句话说,类没有像方法一样的 构造函数,而是将初始化表达式与字段和方法声明交错排列。

按照惯例,类名以 % 结尾。内置根类是 object%。以下表达式创建一个具有公共方法 get-sizegroweat 的类:

(class object%
  (init size)                ; initialization argument
 
  (define current-size size) ; field
 
  (super-new)                ; superclass initialization
 
  (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 字段对方法是可用的。

fish% 中的 (super-new) 表达式调用超类的 初始化。在这种情况下,超类是 object%,它不接受 初始化参数且不执行任何工作;无论如何必须使用 super-new,因为类总是必须调用其超类的初始化。

初始化参数、字段声明和像 (super-new) 这样的表达式 可以在 class 中以任意顺序出现,并且可以与方法声明 交错排列。类中表达式的相对顺序决定了实例化期间的求值顺序。 例如,如果字段的初始值需要调用仅在超类初始化之后才能工作的 方法,则字段声明必须放在 super-new 调用之后。 以这种方式排列字段和初始化声明有助于避免命令式赋值。 方法声明的相对顺序对求值没有影响,因为方法在类实例化之前 就已经完全定义了。

13.1 Methods🔗

fish% 中的三个 define/public 声明各引入一个 新方法。声明使用与 Racket 函数相同的语法,但方法不能作为独立 函数访问。调用 fish% 对象的 grow 方法需要 使用 send 形式:

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

16

fish% 内部,self 方法可以像函数一样调用, 因为方法名在作用域内。例如,fish% 中的 eat 方法直接调用 grow 方法。在类内部, 尝试以方法调用之外的任何方式使用方法名都会导致语法错误。

在某些情况下,类必须调用由超类提供但未被覆盖的方法。 在这种情况下,类可以使用 send 配合 this 来访问该方法:

(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%class 形式时会发出错误信号。相比之下,使用 (send this ....),直到调用 eat-more 方法 并求值 send 形式时才会发出错误信号。因此, inherit 更受欢迎。

send 的另一个缺点是它比 inherit 效率更低。 通过 send 调用方法涉及在运行时在目标对象的类中查找 方法,使得 send 类似于 Java 中基于接口的方法调用。 相比之下,基于 inherit 的方法调用使用类的方法表中 的偏移量,该偏移量在类创建时计算。

为了在从方法所属类的外部调用方法时获得与基于 inherit 的方法调用类似的性能,程序员必须使用 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 是一个覆盖声明。如果 growdefine/public 声明,则在求值 class 表达式 时会发出错误信号,因为 fish% 已经提供了 grow

使用 define/override 还允许通过 super 调用 被覆盖的方法。例如,picky-fish% 中的 grow 实现使用 super 委托给超类的实现。

13.2 Initialization Arguments🔗

由于 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 优先,提供给 newsize 将没有目标声明。

如果 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 Internal and External Names🔗

default-10-fish%size 的两种用法揭示 了类成员标识符的双重身份。当 sizenewsuper-new 中括号对的第一个标识符时, size 是一个 外部名称,以符号方式匹配到 类中的初始化参数。当 size 作为 default-10-fish% 中的表达式出现时, size 是一个词法作用域的 内部名称。 类似地,对继承的 eat 方法的调用将 eat 作为内部名称使用,而 eatsendeat 作为外部名称使用。

class 形式的完整语法允许程序员为类成员指定不同的 内部和外部名称。由于内部名称是局部的,可以重命名它们以避免 遮蔽或冲突。这种重命名并不经常需要,但在没有重命名的情况 下,变通方案可能特别繁琐。

13.4 Interfaces🔗

接口对于检查对象或类是否实现了具有特定(隐含)行为的方法集 很有用。即使没有静态类型系统(这是 Java 有接口的主要原因), 接口的这种使用也是有帮助的。

Racket 中的接口使用 interface 形式创建,它仅声明 实现接口所需的方法名称。接口可以扩展其他接口,这意味着接口 的实现自动实现被扩展的接口。

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

要声明一个类实现了某个接口,必须使用 class* 形式而不是 class

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

例如,与其强制所有鱼类别都派生自 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, and Inner🔗

与 Java 一样,class 形式中的方法可以指定为 final,这意味着子类不能覆盖该方法。final 方法 使用 public-finaloverride-final 声明, 取决于声明是针对新方法还是覆盖实现。

在允许任意覆盖和完全禁止覆盖两个极端之间,类系统还支持 Beta 风格的 可增强方法 [Goldberg04]。 使用 pubment 声明的方法类似于 public, 但该方法不能在子类中被覆盖;只能被增强。 pubment 方法必须使用 inner 显式调用 增强(如果有的话);子类使用 augment 而不是 override 来增强该方法。

一般来说,在类派生中,方法可以在增强和覆盖模式之间切换。 augride 方法规范表示对方法的增强,其中增强本身 在子类中是可覆盖的(尽管超类的实现不能被覆盖)。类似地, overment 覆盖一个方法并使覆盖实现成为可增强的。

13.6 Controlling the Scope of External Names🔗

Java 的访问修饰符(如 protected) 与 define-member-name 的作用类似,但与 Java 不同的是, Racket 的访问控制机制基于词法作用域而非继承层次结构。

Internal and External Names 所述,类成员同时具有内部和外部名称。 成员定义在局部绑定一个内部名称,此绑定可以局部重命名。 相比之下,外部名称默认具有全局作用域,成员定义不绑定外部名称。 相反,成员定义引用外部名称的现有绑定,其中成员名称绑定到 成员键;类最终将成员键映射到方法、字段和 初始化参数。

回顾 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 赋予外部名称一种 package-private 作用域,但从包推广到了 Racket 中的任意 绑定作用域。

例如,下面的 fish%pond% 类通过一个 get-depth 方法协作,该方法只对协作的类可访问:

(define-values (fish% pond%) ; two mutually recursive classes
  (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 的 member-key 绑定。

member-key 值主要与 define-member-name 形式一起使用。 通常,(member-name-key id) 捕获 id 的方法键, 以便可以将其传递给不同作用域中的 define-member-name 使用。这种能力对于泛化 mixin 非常有用,如下所述。

13.7 Interfaces🔗

由于 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 类之间的许多细微差异有助于 mixin 的有效使用。特别是,使用 define/override 明确表示 picky-mixin 期望一个具有 grow 方法的类。如果将 picky-mixin 应用于没有 grow 方法的类,则在应用 picky-mixin 时 立即发出错误信号。

类似地,使用 inherit 在应用 mixin 时强制执行 “方法存在”要求:

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

mixin 的优点是我们可以轻松地组合它们来创建新类, 这些类的实现共享不适合单继承层次结构——没有多重继承 带来的歧义。有了 picky-mixinhungry-mixin,创建一个饥饿但挑剔的鱼就很简单了:

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

关键字初始化参数的使用对于 mixin 的易用性至关重要。例如, picky-mixinhungry-mixin 可以用合适的 eatgrow 方法增强任何类,因为它们 不指定初始化参数,也不在 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]))

最后,对类成员使用外部名称(而不是词法作用域的标识符)使 mixin 使用变得方便。将 picky-mixin 应用于 person% 可以正常工作,因为名称 eatgrow 匹配,无需任何关于 eatgrowfish%person% 中应该是相同方法的先验声明。当成员名称 意外冲突时,此功能是一个潜在的缺点;一些意外冲突可以通过 限制外部名称的作用域来纠正,如 Controlling the Scope of External Names 所述。

13.7.1 Mixins and Interfaces🔗

使用 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 % ....))

接口与 mixin 的另一种用法是为 mixin 生成的类打标签, 以便识别 mixin 的实例。换句话说,is-a? 无法用于 表示为函数的 mixin,但它可以识别由 mixin 一致实现的接口 (有点像 特化接口)。例如, 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 Form🔗

为了将实现 mixin 的 lambdaclass 模式 规范化,包括使用接口指定 mixin 的域和范围,类系统提供了一个 mixin 宏:

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

第一组 interface-expr 确定 mixin 的域,第二组确定 范围。也就是说,展开结果是一个函数,它测试给定的基类是否 实现了第一组 interface-expr,并生成一个实现了第二组 interface-expr 的类。其他要求,如超类中存在 inherit 的方法,则在 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.

mixin 不仅可以覆盖方法和引入公共方法,还可以增强方法、 引入仅增强方法、添加可覆盖的增强以及添加可增强的覆盖—— 类可以做的所有事情(参见 Final, Augment, and Inner)。

13.7.3 Parameterized Mixins🔗

Controlling the Scope of External Names 所述,外部名称可以用 define-member-name 绑定。这种机制允许 mixin 相对于 它定义和使用的方法进行泛化。例如,我们可以将 hungry-mixin 相对于 eat 的外部成员键 进行参数化:

(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 Interfaces🔗

trait 类似于 mixin,它封装了一组要添加到类中的方法。 trait 与 mixin 的不同之处在于,它的各个方法可以使用 trait 操作符进行操作,如 trait-sum(合并两个 trait 的方法)、 trait-exclude(从 trait 中移除一个方法)和 trait-alias(添加方法的副本并使用新名称;不重定向 对旧名称的任何调用)。

mixin 和 trait 之间的实际区别在于,两个 trait 可以组合, 即使它们包含共同的方法,即使任何方法都不能合理地覆盖另一个。 在这种情况下,程序员必须显式解决冲突,通常通过别名方法、 排除方法以及合并使用别名的新 trait。

假设我们的 fish% 程序员想要定义两个类扩展, spotsstripes,每个都包含一个 get-color 方法。鱼的斑点颜色不应该覆盖条纹颜色, 反之亦然;相反,spots+stripes-fish% 应该结合 两种颜色,这在 spotsstripes 被实现 为普通 mixin 时是不可能的。然而,如果 spotsstripes 被实现为 trait,它们可以组合。首先, 我们将每个 trait 中的 get-color 别名为不冲突的名称。 其次,从两者中移除 get-color 方法,合并仅包含别名 的 trait。最后,使用新 trait 创建一个类,该类基于两个别名 引入自己的 get-color 方法,从而得到所需的 spots+stripes 扩展。

13.8.1 Traits as Sets of Mixins🔗

在 Racket 中实现 trait 的一种自然方法是作为一组 mixin, 每个 trait 方法一个 mixin。例如,我们可以尝试按以下方式定义 spots 和 stripes trait,使用关联列表表示集合:

(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 操作符。虽然可以在关联列表中复制一个 mixin,但 mixin 具有固定的方法名,例如 get-color, mixin 不支持方法重命名操作。为了支持 trait-alias,我们必须以与 Parameterized Mixins 中参数化 eat 相同的方式参数化 mixin 的外部方法名。

为了支持 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 方法被移除时, 生成的 trait 等同于

(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)))))

要将 trait T 应用于类 C 并获得派生类, 我们使用 ((trait->mixin T) C)trait->mixin 函数为 T 的每个 mixin 提供 mixin 方法的键和 C 的部分扩展:

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

因此,当上面的 trait 与其他 trait 组合然后应用于一个类时, get-color 的使用变成了对外部名称 get-trait-color 的引用。

13.8.2 Inherit and Super in Traits🔗

trait 的第一个实现支持 trait-alias,并且支持调用 自身的 trait 方法,但不支持相互调用的 trait 方法。特别是, 假设斑点鱼的市场价值取决于其斑点的颜色:

(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 mixin 的作用域 内。实际上,取决于 trait 应用于类时 mixin 的应用顺序, 当 get-price mixin 应用于类时, get-color 方法可能还不可用。因此,在 get-price mixin 中添加 (inherit get-color) 声明并不能解决问题。

一种解决方案是要求在 get-price 等方法中使用 (send this get-color)。这个改变有效,因为 send 总是延迟方法查找,直到方法调用被求值。 然而,延迟查找比直接调用更昂贵。更糟的是,它还延迟了 检查 get-color 方法是否存在的检查。

第二种有效且高效的解决方案是改变 trait 的编码方式。具体来说, 我们将每个方法表示为一对 mixin:一个引入方法,一个实现方法。 当 trait 应用于类时,所有引入方法的 mixin 首先被应用。 然后,实现方法的 mixin 可以使用 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-color get-price %) ....
                (class % ....
                  (define/public (get-price) (void))))
              (lambda (get-color get-price %) ....
                (class % ....
                  (inherit get-color)
                  (define/override (get-price)
                    .... (get-color) ....))))))

通过这种 trait 编码,trait-alias 使用新名称添加 一个新方法,但不改变对旧方法的任何引用。

13.8.3 The trait Form🔗

(require racket/trait) is needed.

通用 trait 模式显然太复杂,不适合程序员直接使用, 但可以很容易地用 trait 宏编码:

(trait trait-clause ...)

可选 inherit 子句中的 id 可以在方法 expr 中直接引用,它们必须由其他 trait 或最终 应用 trait 的基类提供。

将此形式与 trait-sumtrait-excludetrait-aliastrait->mixin 等 trait 操作符一起使用,我们可以按需实现 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 Contracts🔗

由于类是值,它们可以跨越 contract 边界流动,我们可能希望 用 contract 保护给定类的某些部分。为此,使用 class/c 形式。class/c 形式有许多子形式, 描述了两种类型的字段和方法 contract:影响通过实例化对象 使用的 contract 和影响子类的 contract。

13.9.1 External Class Contracts🔗

在其最简单的形式中,class/c 保护从受 contract 约束 的类实例化的对象的公共字段和方法。还有一个 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 字段的对象。 为确保这些条件,我们将使用适当的 contract 定义 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 参数的任何要求。 现在有了受 contract 约束的类,我们可以看到 sizeeat 上的 contract 都得到了执行:

> (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

  expected: an object with the size field

  given: (object)

  fields: none

  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

外部类 contract 有两个重要的注意事项。首先,外部方法 contract 仅在动态分派的目标是受 contract 约束的类的 方法实现时才执行,该实现位于 contract 边界内。覆盖该实现, 从而改变动态分派的目标,将意味着 contract 不再对客户端 执行,因为访问方法不再跨越 contract 边界。与外部方法 contract 不同,外部字段 contract 始终对子类的客户端执行, 因为字段不能被覆盖或遮蔽。

其次,这些 contract 不以任何方式限制 animal% 的 子类。被继承和被子类使用的字段和方法不受这些 contract 检查,通过 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 Internal Class Contracts🔗

请注意,从对象 elephant 检索 size 字段 将 contract 违规归咎于 animal%。这种归咎是 正确的,但对 animal% 类是不公平的,因为我们还没 有为它提供保护自己免受子类影响的方法。为此,我们添加内部 类 contract,它们为子类提供了如何访问和覆盖超类特性的指令。 外部和内部类 contract 之间的这种区别允许在类层次结构内部 使用较弱的 contract,其中不变量可以在内部被子类破坏,但对 通过实例化对象的外部使用应该强制执行。

作为可用保护类型的一个简单示例,我们提供了一个针对 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?)]))

这个类 contract 不仅确保 animal% 类的对象像之前 一样受到保护,还确保 animal% 的子类只在 size 字段中存储适当的值,并适当地使用 animal%size 的实现。这些 contract 形式只影响类层次结构内部的使用,且仅影响跨越 contract 边界的方法调用。

这意味着 inherit 只会影响子类对方法的使用,直到 子类覆盖该方法,而 override 只影响从超类到子类的 覆盖实现的方法调用。由于这些只影响内部使用,当使用这些类 的对象时,override 形式不会自动将子类置于义务中。 此外,override 的使用只在没有发生 Beta 风格增强 的方法上才有意义,因此也只能用于这些方法。下面的示例显示 了这种区别:

(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)))))
(define/contract sloppy-eater%
  (class/c [eat (->m edible/c edible/c)])
  (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: contract violation

  expected: void?

  given: (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: top-level

   (assuming the contract is correct)

  at: eval:47:0

除了这里展示的内部类 contract 形式之外,还有用于 Beta 风格可增强方法的类似形式。inner 形式描述了子类 对给定方法的增强应该提供什么。augmentaugride 都告诉子类,给定方法是一个已被增强的 方法,子类中对该方法的任何调用都将动态分派到超类中的适当 实现。这样的调用将根据给定的 contract 进行检查。这两种形式 的区别在于,使用 augment 表示子类可以增强给定 方法,而使用 augride 表示子类必须覆盖当前的增强。

这意味着并非所有形式都可以同时使用。对于给定方法,只能使用 overrideaugmentaugride 中的一种形式,如果给定方法已被 finalized,则不能使用这些 形式中的任何一种。此外,只有在可以指定 augrideoverride 时,才能为给定方法 指定 super。类似地,只有在可以指定 augmentaugride 时,才能指定 inner