
4.1 类、字段和方法
类是对象的蓝本(blueprint)。一旦定义好一个类,就可以用new关键字根据这个类蓝本创建对象。例如,有了下面这个类定义:

就可以用如下代码创建ChecksumAccumulator的对象:

在类定义中,需要填入字段(field)和方法(method),这些被统称为成员(member)。通过val或var定义的字段是指向对象的变量,通过def定义的方法则包含了可执行的代码。字段保留了对象的状态,或者说数据,而方法用这些数据来对对象执行计算。如果你实例化一个类,则运行时会指派一些内存来保存对象的状态图(即它的变量的内容)。例如,如果你定义了一个ChecksumAccumulator类并给它一个名称为sum的var字段:

然后用如下代码实例化两次:

则内存中这两个对象看上去可能是这个样子的:

由于sum这个定义在ChecksumAccumulator类中的字段是var,而不是val,因此可以在后续代码中对其重新赋予不同的Int值,如:

如此一来,内存中的对象看上去就如同:

关于这张图需要注意的一点是总共有两个sum变量,一个位于acc指向的对象里,而另一个位于csa指向的对象里。字段又叫作实例变量(instance variable),因为每个实例都有自己的变量。这些实例变量合在一起,构成了对象在内存中的映像。从图中不难看出,不仅有两个sum变量,而且当你改变其中一个变量的值时,另一个变量并不会受到影响。
本例中另一个值得注意的是,可以修改acc指向的对象,尽管acc本身是val。由于acc和csa都是val而不是var,因此不能将它们重新赋值并指向其他的对象。例如,如下代码会报错:

因此,能够确定的是,acc永远指向那个你在初始化的时候用的ChecksumAccumulator对象,但随着时间推移,这个对象中包含的字段是有可能改变的。
追求健壮性的一个重要手段是确保对象的状态(它的实例变量的值)在其整个生命周期内都是有效的。首先通过将字段标记为私有(private)来防止外部直接访问字段。因为私有字段只能被定义在同一个类中的方法访问,所有对状态的更新操作的代码都在类的内部。要将某个字段声明为私有,可以在字段前加上private这个访问修饰符,如:

有了这样的ChecksumAccumulator类的定义,任何试图通过外部访问sum的操作都会失败:

注意
在Scala中,使得成员允许公共访问(public)的方式是,不在成员前面显式地给出任何访问修饰符。换句话说,对于那些在Java中可能会用“public”的地方,到了Scala中,什么都不说就对了。公共访问是Scala的默认访问级别。
由于sum是私有的,唯一能访问sum的代码都定义在类自身中。因此,ChecksumAccumulator对于其他人来说没什么用处,除非给它定义一些方法:

ChecksumAccumulator现在有两个方法,即add和checksum,都是函数定义的基本形式,如图2.1(26页)所展示的那样。[1]
传递给方法的任何参数都能在方法内部使用。Scala方法参数的一个重要特征是它们都是val而不是var。[2]因此,如果你试图在Scala的方法中对入参重新赋值,则编译会报错:

虽然在当前版本的ChecksumAccumulator中,add方法和checksum方法正确地实现了预期的功能,但是还可以用更精简的风格来表达。checksum方法最后的return是多余的,可以将其去掉。在没有任何显式的return语句时,Scala方法返回的是该方法计算出的最后一个(表达式的)值。
事实上,我们推荐的方法风格是避免使用任何显式的return语句,尤其是多个return语句。与此相反,尽量将每个方法当作一个最终交出某个值的表达式。这样的哲学鼓励你编写短小的方法,将大的方法拆成小的方法。另一方面,设计中的选择是取决于上下文的,如果你确实想让方法带有多个显式的return,Scala也允许你这样做。
由于checksum方法所做的全部就是计算一个值,因此它并不需要显式的return语句。另一种对方法的简写方式是,当一个方法只会计算一个返回结果的表达式且这个表达式很短时,(方法体)可以被放置在def的同一行。为了极致的精简,还可以省略结果类型,Scala会帮你推断出来。做出这些修改之后,ChecksumAccumulator类看上去是这样的:

在前面的示例中,虽然Scala能够正确地推断出add和checksum这两个方法的结果类型,这段代码的读者也需要通过研读方法体中的代码“在脑海里推断”(mentally infer)这些结果类型。正因如此,通常更好的做法是对类中声明为公有的方法显式地给出结果类型,即使编译器可以帮你推断出来。示例4.1展示了这种风格。
对于结果类型为Unit的方法,如ChecksumAccumulator的add方法,其执行目的是得到副作用。副作用通常是指改变方法外部的某种状态或者执行I/O的动作。对本例的add方法而言,其副作用是给sum重新赋值。那些仅仅因为其副作用而被执行的方法被称作过程(procedure)。

示例4.1 ChecksumAccumulator类的最终版本