类
讲解如何用 define-class 定义类、用 instantiate 实例化对象,以及方法、set! 和 begin 的用法。
要在 OOP 中创建一个对象,你需要实例化(instantiate)一个类。matt-account 和 brian-account 都是「账户」类(class)的实例。
> (define Matt-Account (instantiate account 1000))
Matt-Account
> (define Brian-Account (instantiate account 10000))
Brian-Account
instantiate 函数接受一个类作为第一个参数,返回该类的一个新对象。根据具体类的不同,instantiate 可能需要额外的参数:在这个例子中,你必须在创建账户时指定初始余额。
定义类
面向对象程序中的大部分代码都是各种类的定义。一个类可以看作某类对象的蓝图:「这种对象应该能做什么?它们各自应该知道哪些变量?」。下面是账户类的定义。目前我们只实现一个方法,以后再逐步扩充。这段代码有很多可讲的地方,我们会逐一解释。
(define-class (account balance) ;; 定义一个名为 account 的类
(method (deposit amount)
;; 这个类的对象将拥有一个名为 deposit 的方法
(set! balance (+ amount balance))
balance)
;; deposit 把余额设为当前值加上存入金额,然后返回新的余额
)
define-class
define-class 是一种新的特殊形式,它的语法与 define 类似。原先写过程名的地方,现在写你要定义的类的名字;原先写过程参数的地方,现在写类的初始化变量(instantiation variable)——它们是局部状态变量,其初始值必须作为 instantiate 的额外参数给出。在下面的例子中,初始化变量 balance 被设为 1000。
(define Matt-Account (instantiate account 1000))
类的主体由任意数量的子句组成;这个例子中只有一种子句,即方法(method)子句,稍后我们会学习其他子句。
方法
定义方法的语法也刻意设计得与定义过程相似。方法的「名字」实际上就是访问该方法时使用的消息。当我们说 (ask matt-account 'deposit 50) 时,本质上是在说「在 matt-account 中,找到名为 'deposit 的方法,并用参数 50 调用它」。换句话说,matt-account 会执行 (deposit 50)。
有了当前的类定义,我们其实已经可以执行 (ask matt-account 'balance) 了。有人可能会说:「可是我们还没有定义 balance 方法!」确实如此,但上面的代码照样能跑。对于类中的每个局部状态变量,系统会自动定义一个同名方法。这些方法不接受参数,只返回该变量的当前值。因为我们在实例化 matt-account 时有状态变量 balance,所以免费获得了一个同名的 'balance 方法。这是我们创建「状态」的一种方式,稍后我们会看到另一种方式。
set!
在 deposit 的方法体中,我们引入了一个新过程:set!。这个过程用来改变一个状态变量的值。它的第一个参数不求值,是你想改变的变量的名字;第二个参数要求值,该表达式的值会成为变量的新值。set! 改变变量的值,但它本身不返回任何内容。
> (define a 3)
a
> a
3
> (set! a (+ 2 4))
okay ; 没有返回值时 Scheme 打印的内容
> a
6
> (set! a (+ a a))
okay
> a
12
set! 中的「!」是 Scheme 中表示「会修改某些东西」的函数的命名惯例(就像以「?」结尾的过程会返回 #t 或 #f 的惯例一样)。
笔记
测试你的理解
这看起来和 define 很像,但含义略有不同。define 创建新变量,而 set! 改变已有变量的值。看看下面这段代码:
(define a 10)
(define (change x)
(define a 20)
x)
(change 30)
现在 a 的值是多少?
如果我们改为执行下面这段代码:
(define a 10)
(define (change x)
(set! a 20)
x)
(change 30)
现在 a 的值又是多少?
如果我们试图 set! 一个未定义的变量,会发生什么?
(set! c 10)
到 STk 解释器里试试看。
定义 withdraw 方法
我们已经定义了 deposit 方法,现在来看看如何定义 withdraw 方法。注意,这些方法在类定义中出现的顺序无关紧要。
(define-class (account balance)
(method (deposit amount)
(set! balance (+ amount balance))
balance)
(method (withdraw amount)
(if (< balance amount)
"Insufficient Fund"
(begin
(set! balance (- balance amount))
balance)))
同样,withdraw 是一个接受一个参数 amount 的方法。如果 balance 中的钱不够,就返回 "Insufficient Fund"(余额不足);否则把 balance 减去 amount,并返回剩余的 balance。这里我们用到了一个新的特殊形式:begin。它到底是做什么的?
我该从哪里 begin?
想象一下,如果我们不用 begin 这个特殊形式,你觉得会发生什么?
(if (< balance amount)
"Insufficient Fund"
(set! balance (- balance amount))
balance)
if 只接受 3 个参数:条件、then 分支和 else 分支。如果不用 begin,我们就会有四个参数,解释器会报错。到目前为止,在我们求值的每个过程中都只有一个表达式,用它来提供过程的返回值。一个过程仍然只能返回一个值。但是现在,我们有时希望为了表达式的「副作用」而不是「返回值」去求值它,比如改变某个变量的值。调用 begin 表示 (set! amount (- amount balance)) 和 balance 共同构成 if 的单个参数。
下一节,我们将学习一个类如何继承另一个类的属性。