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,都是函数定义的基本形式,如图3.1(36页)所展示的那样。
传递给方法的任何参数都能在方法内部使用。Scala方法参数的一个重要特征是它们都是val而不是var。[1]因此,如果你试图在Scala的方法中对入参重新赋值,编译会报错:
虽然当前版本的ChecksumAccumulator中,add和checksum正确地实现了预期的功能,还可以用更精简的风格来表达。首先,checksum方法最后的return是多余的,可以去掉。在没有任何显式的return语句时,Scala方法返回的是该方法计算出的最后一个(表达式的)值。
事实上,我们推荐的方法风格是避免使用任何显式的return语句,尤其是多个return语句。与此相反,尽量将每个方法当作一个最终交出某个值的表达式。这样的哲学鼓励你编写短小的方法,将大的方法拆成小的。另一方面,设计中的选择也是取决于上下文的,Scala也允许你方便地编写有多个显式return的方法,如果那确实是你想要的。
由于checksum所做的全部就是计算一个值,它并不需要显式的return。另一种对于方法的简写方式是,当一个方法只会计算一个返回结果的表达式时,可以不写花括号。如果这个表达式很短,它甚至可以被放置在def的同一行。为了极致的精简,还可以省略结果类型,Scala会帮你推断出来。做出这些修改之后,ChecksumAccumulator类看上去是这样的:
在前面的示例中,虽然Scala能够正确地推断出add和checksum这两个方法的结果类型,这段代码的读者也需要通过研读方法体中的代码在脑海里推断(mentally infer)这些结果类型。正因如此,通常更好的做法是对类中声明为公有的方法显式地给出结果类型,哪怕编译器可以帮你推断出来。示例4.1展示了这种风格:
示例4.1 ChecksumAccumulator类的最终版本
对于结果类型为Unit的方法,如ChecksumAccumulator的add方法,执行的目的是为了得到其副作用。副作用通常来说指的是改变方法外部的某种状态或者执行I/O的动作。对本例的add而言,其副作用是给sum重新赋值。那些仅仅因为其副作用而被执行的方法被称作过程(procedure)。