![Scala编程(第4版)](https://wfqqreader-1252317822.image.myqcloud.com/cover/353/38381353/b_38381353.jpg)
4.1 类、字段和方法
类是对象的蓝本(blueprint)。一旦你定义好一个类,就可以用new关键字从这个类蓝本创建对象。例如,有了下面这个类定义:
![](https://epubservercos.yuewen.com/E8DF3B/20205397808551606/epubprivate/OEBPS/Images/40272-00-89-1.jpg?sign=1739284293-nkIw37BlJ86PTLHvnY3E68a7WrVQZMlE-0-7dddec4613799ced84f6d9f988aadafb)
就可以用如下代码创建ChecksumAccumulator的对象:
![](https://epubservercos.yuewen.com/E8DF3B/20205397808551606/epubprivate/OEBPS/Images/40272-00-89-2.jpg?sign=1739284293-aOXOQN0VNBSJvtzgs9wSS9XRRQJE9GlQ-0-dda1c8d619b6783c5594a26fa9f3d5bb)
在类定义中,你会填入字段(field)和方法(method),这些被统称为成员(member)。通过val或var定义的字段是指向对象的变量,通过def定义的方法则包含了可执行的代码。字段保留了对象的状态,或者说数据,而方法用这些数据来对对象执行计算。当你实例化一个类,运行时会指派一些内存来保存对象的状态图(即它的变量的内容)。例如,如果你定义了一个ChecksumAccumulator类并给它一个名为sum的var字段:
![](https://epubservercos.yuewen.com/E8DF3B/20205397808551606/epubprivate/OEBPS/Images/40272-00-90-1.jpg?sign=1739284293-rkkK5xnzvPXCfibIwtoqjXLbgYcXT257-0-9691a46eeb1cf12a10e31393abc5413c)
然后用如下代码实例化两次:
![](https://epubservercos.yuewen.com/E8DF3B/20205397808551606/epubprivate/OEBPS/Images/40272-00-90-2.jpg?sign=1739284293-FsMMPSNo8ksDjv1huV7P1DsZ7gfMR60Z-0-eea57a96b5798dd65a08d873d81e9e22)
那么内存中这两个对象看上去可能是这个样子的:
![](https://epubservercos.yuewen.com/E8DF3B/20205397808551606/epubprivate/OEBPS/Images/40272-00-90-3.jpg?sign=1739284293-9fDx2pezc88QXUtxoPepGsuAT5qT7GqE-0-8a0c59dae7e778ea6b3224efe2af5589)
由于sum这个定义在ChecksumAccumulator类中的字段是var,而不是val,可以在后续代码中对其重新赋予不同的Int值,如:
![](https://epubservercos.yuewen.com/E8DF3B/20205397808551606/epubprivate/OEBPS/Images/40272-00-90-4.jpg?sign=1739284293-aVBospD88U0xwWljKwfbK3VXJQjBq0jk-0-73ebcb92c966219e7da12f8c4f390c01)
如此一来,内存中的对象看上去就如同:
![](https://epubservercos.yuewen.com/E8DF3B/20205397808551606/epubprivate/OEBPS/Images/40272-00-91-1.jpg?sign=1739284293-EA7cMQcN5F71ay0AELXHZfaFL8518ns1-0-cd35fa56f5c37efd237ab180f20b6537)
关于这张图需要注意的一点是总共有两个sum变量,一个位于acc指向的对象里,而另一个位于csa指向的对象里。字段又叫作实例变量(instance variable),因为每个实例都有自己的变量。这些实例变量合在一起,构成了对象在内存中的映像。从图中不难看出,不光是有两个sum变量,而且当你改变其中一个的值的时候,另一个并不会受到影响。
本例中另一个值得注意的是可以修改acc指向的对象。尽管acc本身是val,由于acc和csa都是val而不是var,你不能做的是将它们重新赋值指向别的对象。例如,如下代码会报错:
![](https://epubservercos.yuewen.com/E8DF3B/20205397808551606/epubprivate/OEBPS/Images/40272-00-91-2.jpg?sign=1739284293-yKGzESgPegT2T75iQNoiDvydN5W3gsh1-0-ed1883ece43fe35dc93575bfe95854ce)
因此,你能够确信的是,acc永远指向那个你在初始化的时候用的ChecksumAccumulator对象,但随着时间推移这个对象中包含的字段是有可能改变的。
追求健壮性的一个重要手段是确保对象的状态(它的实例变量的值)在其整个生命周期都是有效的。首先是通过将字段标记为私有(private)来防止外部直接访问字段。因为私有字段只能被定义在同一个类中的方法访问,所有对状态的更新操作的代码,都在类的内部。要将某个字段声明为私有,可以在字段前加上private这个访问修饰符,如:
![](https://epubservercos.yuewen.com/E8DF3B/20205397808551606/epubprivate/OEBPS/Images/40272-00-91-3.jpg?sign=1739284293-m7LlODxy6iAlpf6nGCeB8ij9Go6SB4lC-0-c995ede8602cab55b7fd1be9faa557cf)
有了ChecksumAccumulator的定义,任何试图通过外部访问sum的操作都会失败:
![](https://epubservercos.yuewen.com/E8DF3B/20205397808551606/epubprivate/OEBPS/Images/40272-00-92-1.jpg?sign=1739284293-KT2s7nlrtqmDaENLR1V3WOKaW5rcSYRm-0-aaa10be5fb8d38c5f2c65608e17c2ae4)
注意
在Scala中,使得成员允许公共访问(public)的方式是,不在成员前面显式地给出任何访问修饰符。换句话说,在那些在Java中可能会用“public”的地方,到了Scala中,什么都不说就对了。公共访问是Scala的默认访问级别。
由于sum是私有的,唯一能访问sum的代码都定义在类自己里面。因此,ChecksumAccumulator对于别人来说没什么用处,除非给它定义一些方法:
![](https://epubservercos.yuewen.com/E8DF3B/20205397808551606/epubprivate/OEBPS/Images/40272-00-92-2.jpg?sign=1739284293-5coqzyfN2QVVJ7SI7W5EhbQEcodkW1cr-0-3148d57cf3fdf584b68cd31556d99905)
ChecksumAccumulator现在有两个方法,add和checksum,都是函数定义的基本形式,如图3.1(36页)所展示的那样。
传递给方法的任何参数都能在方法内部使用。Scala方法参数的一个重要特征是它们都是val而不是var。[1]因此,如果你试图在Scala的方法中对入参重新赋值,编译会报错:
![](https://epubservercos.yuewen.com/E8DF3B/20205397808551606/epubprivate/OEBPS/Images/40272-00-93-1.jpg?sign=1739284293-9RJE7OOLyAogAFM4sasigJXyuzcPHugM-0-68576feec04a73a835f2f8ddd209d8e7)
虽然当前版本的ChecksumAccumulator中,add和checksum正确地实现了预期的功能,还可以用更精简的风格来表达。首先,checksum方法最后的return是多余的,可以去掉。在没有任何显式的return语句时,Scala方法返回的是该方法计算出的最后一个(表达式的)值。
事实上,我们推荐的方法风格是避免使用任何显式的return语句,尤其是多个return语句。与此相反,尽量将每个方法当作一个最终交出某个值的表达式。这样的哲学鼓励你编写短小的方法,将大的方法拆成小的。另一方面,设计中的选择也是取决于上下文的,Scala也允许你方便地编写有多个显式return的方法,如果那确实是你想要的。
由于checksum所做的全部就是计算一个值,它并不需要显式的return。另一种对于方法的简写方式是,当一个方法只会计算一个返回结果的表达式时,可以不写花括号。如果这个表达式很短,它甚至可以被放置在def的同一行。为了极致的精简,还可以省略结果类型,Scala会帮你推断出来。做出这些修改之后,ChecksumAccumulator类看上去是这样的:
![](https://epubservercos.yuewen.com/E8DF3B/20205397808551606/epubprivate/OEBPS/Images/40272-00-93-2.jpg?sign=1739284293-mzM5uvwvt9jG6LH8YFDr7MhUaYoCLCpi-0-c6867b4401fd9068baf796e4fdee9e1b)
在前面的示例中,虽然Scala能够正确地推断出add和checksum这两个方法的结果类型,这段代码的读者也需要通过研读方法体中的代码在脑海里推断(mentally infer)这些结果类型。正因如此,通常更好的做法是对类中声明为公有的方法显式地给出结果类型,哪怕编译器可以帮你推断出来。示例4.1展示了这种风格:
![](https://epubservercos.yuewen.com/E8DF3B/20205397808551606/epubprivate/OEBPS/Images/40272-00-94-1.jpg?sign=1739284293-g0S0WlcY0dfFTBtv7bDNGjrYXhygIo2k-0-30c405ceda670f2ee85b8f4f196812bf)
示例4.1 ChecksumAccumulator类的最终版本
对于结果类型为Unit的方法,如ChecksumAccumulator的add方法,执行的目的是为了得到其副作用。副作用通常来说指的是改变方法外部的某种状态或者执行I/O的动作。对本例的add而言,其副作用是给sum重新赋值。那些仅仅因为其副作用而被执行的方法被称作过程(procedure)。