返回介绍

6.3 字段和继承

发布于 2025-02-20 00:17:08 字数 5818 浏览 0 评论 0 收藏 0

来看一下我们目前是怎么处理对象创建的:

[(create)
 (make-obj class
           (make-hash (list (cons 'f init) ...)))]

问题就在这里:在字典中我们只初始化了当前类声明的字段的值!还需要对祖先类的字段值进行初始化。

6.3.1 继承字段

对象应该包含其祖先声明的所有字段的值。因此,当创建类时,我们应该确定它的实例的所有字段。要做到这一点,我们必须扩展类,使其保留所有字段的列表,并能够将该信息提供给任何需要的子类。

(defmac (CLASS extends superclass
               ([field f init] ...)
               ([method m params body] ...))
  #:keywords field method extends
  #:captures self ? !
  (let* ([scls superclass]
         [methods ....]
         [fields (append (scls 'all-fields)
                         (list (cons 'f init) ...))])
    (letrec
        ([class (λ (msg . vals)
                  (case msg
                    [(all-fields) fields]
                    [(create) (make-obj class
                                        (make-hash fields))]
                    ....))]))))

在类的词法环境中,我们引入新的 fields 标识符。该标识符绑定到类的实例应该有的全部字段的列表。要获取超类的所有字段,只要向其发送 all-fields 消息(其实现简单地返回绑定到 fields 的表)。创建对象时,我们就要用这些字段来创建新的字典。

因为我们给类的词汇表增加了新消息,所以需要想想如果 Root 收到这个消息该怎么处理:它的所有字段是什么?必须是空表,因为我们不加分辨地使用了 append

(define Root
  (λ (msg . vals)
    (case msg
      [(lookup)     (error "message not understood:" (first vals))]
      [(all-fields) '()]
      [else (error "root class: should not happen: " msg)])))

来试试这是否有效:

> (define cp (new ColorPoint))
> (-> cp color! 'red)
> (-> cp color?)
'red
> (-> cp move 5)
> (-> cp x?)
5

太好了!

6.3.2 字段的绑定

实际上,还有一个问题我们没有考虑过:如果子类定义了一个字段,其名字已经存在于其祖先之一,会发生什么?

(define A
 (CLASS extends Root
        ([field x 1]
         [field y 0])
        ([method ax () (? x)])))
(define B
  (CLASS extends A
         ([field x 2])
         ([method bx () (? x)])))

> (define b (new B))
> (-> b ax)
2
> (-> b bx)
2

在这两种情况下,返回的都是绑定到 Bx 字段的值。换句话说,和方法一样,字段也是延迟绑定的。这合理吗?

强封装

我们来想一想:对象的目的是将一些(可能可变的)状态封装在适当的程序接口(方法)之后。显然,对方法延迟绑定是理想的,因为方法是对象的外部接口。那么字段呢?字段应该是隐藏的、对象的内部状态——换种说法,实现的细节,而不是公开的接口。其实,请注意我们的语言到目前为止,甚至不能访问另一个对象除 self 之外的的字段!那么,至少,对字段的延迟绑定是值得疑问的。

私有方法应该延时绑定吗? 他们是延迟绑定的吗?

来看一下 委托 是怎么处理字段的?那里,字段只是函数的自由变量,所以它们遵从 词法作用域 。对字段来说,这是更合理的语义。在类中定义方法时,其根据该类中直接定义的字段或其超类中的字段。这里的道理是,因为所有这些都是在编写类定义的时候已知的信息。延迟绑定字段意味着对方法中的所有自由变量重新引入了动态作用域:有趣的错误之源和头痛的来源!(想想这样的例子,子类意外地引入与超类中已有名称一样的字段,从而导致混乱。)

6.3.3 字段遮蔽

本节讨论如何定义被称为 字段遮蔽 (field shadowing)的语义:类的字段遮蔽超类的同名字段,但是方法总是访问它所在的类或其祖先声明的字段。

具体来说,这意味着一个对象可以为同名字段保存不同的值;使用哪一个取决于具体执行的方法在哪个类定义(这被称为方法的 宿主类 (host class))。由于这种多重性,只用一个哈希表是不够了。替代方案,我们在类中保存一份字段名称的列表,并在对象中保存由值组成的 向量 (vector),通过位置访问向量中的值。字段访问将分两步完成:首先根据名称列表确定字段的位置,然后访问对象中值向量对应位置的值。

例如,对于上面的类 A ,名称列表是 '(x y)A 一个实例的值向量是 #(1 0) 。对于 B 类,名称列表是 '(x y x) ,一个实例的值向量是 #(1 0 1) 。以这种方式保持字段的优点是,在没有遮蔽的情况下,字段总是在对象内相同的位置中。

要遵从遮蔽的语义,我们(至少)有两个选项。一种方法,我们可以将被遮蔽字段重命,例如 B 中的字段名变成 '(x0 y x) ,这样 B 中的方法及其后代只能看到 x ——也就是 B 中引入的字段——的最新定义。另一种方法是保持字段名不变,查找从字段列表尾部开始:也就是说,我们希望在名称列表中找到字段名 最后 的位置。这里我们选择后一种方案。

修改 CLASS 的定义,以引入向量和字段查找策略:

....
[(create)
 (let ([values (list->vector (map cdr fields))])
   (make-obj class values))]
[(read)
 (vector-ref (obj-values (first vals))
             (find-last (second vals) fields))]
[(write)
 (vector-set! (obj-values (first vals))
              (find-last (second vals) fields)
              (third vals))]
....

创建对象时,我们用初始字段值构造向量。然后,访问字段时,我们用 find-last 返回的位置来访问此向量。不过,试一下就知道,此路不通!语义和之前一样,还是错误的。

为什么呢?回忆一下我们是怎么处理字段访问的,即怎么去除 ? 语法糖:

(defmac (? fd) #:captures self
  ((obj-class self) 'read self 'fd))

这里写的表达式是,先询问 self 是哪个类,然后发送給该类 read 消息。嗯,但是 self 是动态绑定到接收方对象的,所以我们总是在要求原来的类访问字段!错误在这里。不应将 read 消息发送给接收方的类,而是发送给方法的 宿主类 。怎么实现呢?需要一种方法,从方法体找到它的宿主类,或者更好的办法,直接访问宿主类的字段列表。

我们可以将字段列表放在方法的词法环境中,就像 self 那样,但这样的话程序员可能会意外地影响绑定(与之相反, self 一般是面向对象语言中的关键字)。字段列表(以及绑定它的名称)应该是我们的实现内部的东西。既然我们在类中局部定义了 ?! ,可以简单地将字段列表 fields 限定在这些语法定义的范围内;由宏观的卫生扩展来确保用户代码不可能意外地影响 fields

....
(let* ([scls superclass]
       [fields (append (scls 'all-fields)
                       (list (cons 'fd val) ...))]
       [methods
        (local [(defmac (? fd) #:captures self
                  (vector-ref (obj-values self)
                              (find-last 'fd fields)))
                (defmac (! fd v) #:captures self
                  (vector-set! (obj-values self)
                               (find-last 'fd fields)
                               v))]
          ....)]))

这个实现并不理想,因为每次字段访问都会调用 find-last (昂贵/线性开销)。可以避免吗? 如何避免?

请注意,我们现在直接访问 fields 表,所以无需再向类发送字段访问消息。对于写入字段也是一样。

来试试这一切是否能按预期运行:

(define A
 (CLASS extends Root
        ([field x 1]
         [field y 0])
        ([method ax () (? x)])))
(define B
  (CLASS extends A
         ([field x 2])
         ([method bx () (? x)])))

> (define b (new B))
> (-> b ax)
1
> (-> b bx)
2

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文