继承

讲解 OOP 中的继承机制:子类如何通过 parent 子句继承父类的方法,以及 self 关键字和变量作用域规则。

引言

可以想象,随着 OOP 程序越来越大,你会定义越来越多的对象和类。其中一些类会具有相似的特征。例如,你可能有一个 box 类、一个 safety-deposit-box 类和一个 locked-box 类。它们都需要类似的方法,比如往里面放东西和从里面取东西。如果每种类都重新写一遍代码,那就太冗余了。我们想要的是:定义一个通用类(比如 box 类),让它知道像 open(打开)这样的通用方法,然后让更具体的类(比如 safe-deposit-box 类)继承(inherit)通用 box 类。

父类与子类

假设我们要创建一个 checking-account(支票账户)类。支票账户和普通银行账户一样,只是你还能开支票,而不必亲自去取钱。但每开一次支票,会被收取十美分费用。

> (define Hal-Account (instantiate checking-account 1000))
Hal-Account
> (ask Hal-Account 'balance)
1000
> (ask Hal-Account 'deposit 100)
1100
> (ask Hal-Account 'withdraw 50)
1050
> (ask Hal-Account 'write-check 30)
1019.9

实现 checking-account 的一种办法是把 account 类的所有代码复制一份。但这样一来,如果我们之后需要修改 account,就得记着同步修改 checking-account。

在面向对象编程中,一个类是另一个类的特化,这是非常常见的情况:新类拥有旧类的全部方法,再加上一些额外的方法,就像这个银行账户的例子一样。为了描述这种情况,我们使用对象类「家族」的比喻:原来的类是父类(parent),特化出来的版本是子类(child)类。我们说子类继承父类的方法。(子类也称 subclass,父类也称 superclass。)

父类

下面是如何创建 account 类的子类:

(define-class (checking-account init-balance)
    (parent (account init-balance))
    (method (write-check amount)
        (ask self 'withdraw (+ amount 0.10)) ))

这个例子引入了 define-class 中的 parent(父类)子句。这里父类是 account 类。注意,由于 account 类需要一个实例化变量,我们也必须提供相应的参数(因此写作 (account init-balance))。

当我们给 checking-account 对象发送消息时,对应的方法从哪里来?如果 checking-account 类中定义了同名方法,就使用它;否则,OOP 系统会去父类 account 中查找该方法。如果父类也没有,我们就继续查看父类的父类,依此类推。

笔记

测试你的理解

以下问题基于上面 account 和 checking-account 的类定义。

(define sam (instantiate checking-account 500))

下面哪些做法会返回错误?

self 关键字

write-check 应该做什么?它应该把账户余额减去指定金额,再扣除额外费用。减少余额的方法我们已经会了,就是 withdraw 方法!要在另一个方法的方法体中调用已定义的方法,我们使用 self,即 (ask self 'withdraw (+ amount 0.10))。每个对象都有一个值为自身的局部状态变量 self。

作用域

在某个类中定义的方法只能访问同一个类中定义的局部状态变量。例如,在 checking-account 类中定义的方法不能引用在 account 类中定义的 balance 变量;同样,account 类中的方法也不能引用 init-balance 变量。

这条规则对应 Scheme 中关于变量作用域的通常规则:每个变量只在定义它的块内可用。(顺便说一句,并非所有 OOP 实现都是这样设计的。)

笔记

测试你的理解

类真是太棒了!它们让对象井然有序。继承也很棒!它让类井然有序。请留意一个子类拥有哪些状态、哪些状态会被更新。

> (define nick (instantiate checking-account 500))
> (ask nick 'init-balance)
500
> (ask nick 'balance)
500
> (ask nick 'deposit 50)
550

下面的表达式返回什么?

(ask nick 'balance)

下面的表达式返回什么?

(ask nick 'init-balance)

假设现在我们有下面这段代码:

(define-class (checking-account init-balance)
    (parent (account init-balance))
    (method (write-check amount)
        (ask self 'withdraw (+ amount 0.10)) )
    (method (show-balance) balance)  )

(define jeffrey (instantiate checking-account 500))

我们给这个类新增了一个方法 show-balance。(ask jeffrey 'show-balance) 会返回什么?

要点回顾

本小节的几个要点:

  • 有些类是另一个类的「特化」或「具体化」版本。在这种情况下,我们希望让具体的类成为「父」类的「子」类。
  • 子类继承父类的所有方法。
  • 随时留意你的类中哪些变量真正在作用域内。

接下来

我们要学习一个类可以拥有哪些种类的变量。