继承
讲解 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) 会返回什么?
要点回顾
本小节的几个要点:
- 有些类是另一个类的「特化」或「具体化」版本。在这种情况下,我们希望让具体的类成为「父」类的「子」类。
- 子类继承父类的所有方法。
- 随时留意你的类中哪些变量真正在作用域内。
接下来
我们要学习一个类可以拥有哪些种类的变量。