13 类和对象

本章基于一篇论文[Flatt06]。

一个类(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 接口

接口对于检查一个对象或一个类实现一组具有特定(隐含)行为的方法非常有用。接口的这种使用有帮助的,即使没有静态类型系统(那是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 控制外部名称的范围

java的访问修饰符(如受保护的(protected))扮演的一个角色类似于define-member-name,但不像java,访问控制Racket的机制是基于词法范围,不能继承层次结构。

正如内部和外部名称所指出的,类成员既有内部名称,也有外部名称。成员定义在本地绑定内部名称,此绑定可以在本地重命名。与此相反,外部名称默认情况下具有全局范围,成员定义不绑定外部名称。相反,成员定义指的是外部名称的现有绑定,其中成员名绑定到成员键(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 混合

因为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 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 参数化的混合

正如在《控制外部名称的范围》中指出的,外部名称可以用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)类似于一个混合(mixin),它封装了一组方法添加到一个类里。一个特征不同于一个混合,它自己的方法是可以用特征运算符操控的,比如trait-sum(合并这两个特征的方法)、trait-exclude(从一个特征中移除方法)以及trait-alias(添加一个带有新名字的方法的拷贝;它不重定向到对任何旧名字的调用)。

混合和特征之间的实际差别是两个特征可以组合,即使它们包括了共有的方法,而且即使两者的方法都可以合理地覆盖其它方法。在这种情况下,程序员必须明确地解决冲突,通常通过别名方法、排除方法、以及合并使用别名的新特性。

假设我们的fish%程序想要定义两个类扩展,spots和stripes,每个都包含get-color方法。fish的spot-color不应该重写stripe-color,反之亦然;相反,一个spots+stripes-fish%应结合两种颜色,如果spots和stripes是普通混合实现,那这是不可能的。然而,如果spots和stripes作为特征来实现,它们可以组合在一起。首先,我们在每个特征中给get-color起一个别名为一个不冲突的名称。第二,get-color方法从两者中移除,只有别名的特征被合并。最后,新特征用于创建一个类,它基于这两个别名引入自己的get-color方法,生成所需的spots+stripes扩展。

13.8.1 特征作为混合集

在Racket里实现特征的一个自然的方法是作为混合的一个集合,每个特征方法带一个混合。例如,我们可以尝试如下定义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)))))

我们用((trait->mixin T) C)给类C应用特征T并获得一个派生类。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 ...)

The ids in the optional inherit clause are available for direct reference in the method exprs, and they must be supplied either by other traits or the base class to which the trait is ultimately applied. 在可选的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表不会自动将子类插入到合约中。此外,使用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: 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: (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才可以被指定。

Racket编程指南——13 类和对象相关推荐

  1. Java面向对象编程篇1——类与对象

    Java面向对象编程篇1--类与对象 1.面向过程 1.1.概念 面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了 1.2.优缺点 优点:性 ...

  2. C++学习笔记 - 阶段三:C++核心编程 - Chapter7:类和对象-C++运算符重载

    阶段三:C++核心编程 Chapter7:类和对象-C++运算符重载 运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型 7.1 加号运算符重载 作用:实现两个自定义数 ...

  3. C++核心编程:P10->类和对象----多态

    本系列文章为黑马程序员C++教程学习笔记,前面的系列文章链接如下 C++核心编程:P1->程序的内存模型 C++核心编程:P2->引用 C++核心编程:P3->函数提高 C++核心编 ...

  4. python类和对象介绍_Python开发基础-Day17面向对象编程介绍、类和对象

    面向对象变成介绍 面向过程编程 核心是过程(流水线式思维),过程即解决问题的步骤,面向过程的设计就好比精心设计好一条流水线,考虑周全什么时候处理什么东西.主要应用在一旦完成很少修改的地方,如linux ...

  5. JAVA编程中的类和对象

    1:初学JAVA,都知道JAVA是面向对象的编程.笔者这节开始说说类和对象.(实例仅供参考,如若复制粘贴记得修改包名和类名,避免出错) 学习JAVA的快捷键,Alt+/代码补全功能,其实此快捷键启动了 ...

  6. 面向对象编程思想 以及类与对象

    一.面向对象编程思想 众所周知,我们常见的编程思想有面向过程和面向对象两种,像我们最基础的c语言,就是一种以过程为中心的编程思想,不关注具体的事件和对象而是针对于解决问题的思路和目标,这种编程思想由于 ...

  7. 面向对象:编程范式、类、对象

    编程范式: 1. 面向过程编程: 核心是"过程","过程"指的是解决问题的步骤:就相当于在设计一条流水线 优点:复杂问题流程化,进而简单化 缺点:可扩展性差,前 ...

  8. java类与对象 编程题目_Java类与对象的课后练习

    Java类与对象的课后练习编程题(java2实用教程P111) 这章内容自学完了,在做教材课后其他练习题的时候(只要自己仔细)都没啥问题,但在做最后一道上机编程题的时候问题出现了,在阅读题目的时候自己 ...

  9. 紫影龙的编程日记 —— 认识类和对象

    今天是我学习 Visual C++ 开发技术的第二天,我主要学习的是类和对象.在学习类和对象之前,首先需要了解两个概念. 面向过程程序设计 ( Procedural Programming ) 方法是 ...

最新文章

  1. 没水?没电?从非洲难民到美国华盛顿知名游戏开发者,有梦想的人,世界会为他让路!...
  2. MyBatis学习总结(五)——实现关联表查询
  3. Mysql:Sql的执行顺序
  4. everything文件搜索_Everything,闪电搜索,百万文件100%秒搜,真是文件搜索神器!...
  5. 【Python】安装配置Anaconda
  6. [html] 写一个三栏布局,中间固定,两边自适应(平均)
  7. Yoast SEO wordpress插件 + 所有扩展
  8. Chapter 15 配置服务器存储和群集 第1课
  9. (1)win10 64位系统ISE14.7闪退问题(FPGA不积跬步101)
  10. 电脑桌面云便签怎么将界面最小化?
  11. 为什么会有带www的域名和不带www的域名
  12. 数据结构严蔚敏--综述
  13. android textview 淡入淡出,TextView淡入淡出效果
  14. 爬虫晋江小说python_python 爬虫入门之爬小说
  15. Linux /usr/src/kernels 目录为空的解决方法
  16. 加盟连锁如何降低风险?
  17. C语言程序设计入门——水仙花数
  18. 抖音网页版入口登录链接地址
  19. Java 调用 有道翻译API
  20. 公共区域U盘窃取数据

热门文章

  1. 轻松看透WeX5产品能力和技术
  2. MySQL主从同步详解与配置
  3. 如何把SQL表格发给别人直接使用
  4. 中国成为论文发表数量第一的国家
  5. 【光剑藏书轩2021】5分钟读懂《贫穷的本质》:“穷人通常缺少信息来源”
  6. 这十个嵌入式工程师最青睐的树莓派扩展板让你受用半生
  7. Mandriva 2009 Spring PWP中3D桌面的使用
  8. 湘潭大学计算机组成原理试卷,湘潭大学 计算机组成与原理 控制器实验 实验报告...
  9. 腾讯视频转换mp4格式用什么转换器?电脑怎么把腾讯视频转换成mp4?
  10. JS判断ios系统的版本号