分号推断
Scala程序里,语句末尾的分号通常是可选的。如果你愿意可以输入一个,但若一行里仅有一个语句也可不写。另一方面,如果一行里写多个语句那么分号是需要的:
51CTO编辑推荐:Scala编程语言专题
- val s = "hello"; println(s)
如果你想输入一个跨越多行的语句,多数时候你只需输入,Scala将在正确的位置分隔语句。例如,下面的代码被认为是一个跨四行的语句:
然而,偶尔Scala也许没有按照你的愿望把句子分割成两部分:
- if (x < 2)
- println("too small")
- else
- println("ok")
这会被分成两个语句x和+ y。如果你希望把它作为一个语句x + y,你可以把它包裹在括号里:
- x
- + y
或者,你也可以把+放在行末。正是由于这个原因,当你在串接类似于+的中缀操作符,把操作符放在行尾而不是行头是普遍的Scala风格:
- (x
- + y)
分号推断的规则
- x +
- y +
- z
分割语句的精确规则非常有效却出人意料的简单。那就是,除非以下情况的一种成立,否则行尾被认为是一个分号:
1.疑问行由一个不能合法作为语句结尾的字结束,如句点或中缀操作符。
2.下一行开始于不能作为语句开始的字。
3.行结束于括号(...)或方框[...]内部,因为这些符号不可能容纳多个语句。
Singleton对象
如第1章所提到的,Scala比Java更面向对象的一个方面是Scala没有静态成员。替代品是,Scala有单例对象:singleton object。除了用object关键字替换了class关键字以外,单例对象的定义看上去就像是类定义。代码4.2展示了一个例子:
代码 4.2 类ChecksumAccumulator的伴生对象
- // 文件ChecksumAccumulator.scala
- import scala.collection.mutable.Map
- object ChecksumAccumulator {
- private val cache = Map[String, Int]()
- def calculate(s: String): Int =
- if (cache.contains(s))
- cache(s)
- else {
- val acc = new ChecksumAccumulator
- for (c <- s)
- acc.add(c.toByte)
- val cs = acc.checksum()
- cache += (s -> cs)
- cs
- }
- }
表中的单例对象被叫做ChecksumAccumulator,与前一个例子里的类同名。当单例对象与某个类共享同一个名称时,他被称作是这个类的伴生对象:companion object。你必须在同一个源文件里定义类和它的伴生对象。类被称为是这个单例对象的伴生类:companion class。类和它的伴生对象可以互相访问其私有成员。
ChecksumAccumulator单例对象有一个方法,calculate,用来计算所带的String参数中字符的校验和。它还有一个私有字段,cache,一个缓存之前计算过的校验和的可变映射。这里我们使用了缓存例子来说明带有域的单例对象。像这样的缓存是通过内存换计算时间的方式做到性能的优化。通常意义上说,只有遇到了缓存能解决的性能问题时,才可能用到这样的例子,而且应该使用弱映射(weak map),如scala.Collection.jcl的WeakHashMap,这样如果内存稀缺的话,缓存里的条目就会被垃圾回收机制回收掉。 方法的第一行,“if (cache.contains(s))”,检查缓存,看看是否传递进来的字串已经作为键存在于映射当中。如果是,就仅仅返回映射的值,“cache(s)”。否则,执行else子句,计算校验和。else子句的第一行定义了一个叫acc的val并用新建的ChecksumAccumulator实例初始化它。因为关键字new只用来实例化类,所以这里创造的新对象是ChecksumAccumulator类的一个实例,而不是同名的单例对象。下一行是个for表达式,对传入字串的每个字符循环一次,并在其上调用toByte把字符转换成Byte,然后传递给acc所指的ChecksumAccumulator实例的add方法。完成了for表达式后,下一行的方法在acc上调用checksum,获得传入字串的校验和,并存入叫做cs的val。下一行,“cache += (s -> cs)”,传入的字串键映射到整数的校验和值,并把这个键-值对加入cache映射。方法的最后一个表达式,“cs”,保证了校验和为此方法的结果。
如果你是Java程序员,考虑单例对象的一种方式是把它当作是或许你在Java中写过的任何静态方法之家。可以在单例对象上用类似的语法调用方法:单例对象名,点,方法名。例如,可以如下方式调用ChecksumAccumulator单例对象的calculate方法:
- ChecksumAccumulator.calculate("Every value is an object.")
然而单例对象不只是静态方法的收容站。它同样是个第一类的对象。因此你可以把单例对象的名字看作是贴在对象上的“名签”:
定义单例对象不是定义类型(在Scala的抽象层次上说)。如果只是ChecksumAccumulator对象的定义,你就建不了ChecksumAccumulator类型的变量。宁愿这么说,ChecksumAccumulator类型是由单例对象的伴生类定义的。然而,单例对象扩展了超类并可以混入特质。由于每个单例对象都是超类的实例并混入了特质,你可以通过这些类型调用它的方法,用这些类型的变量指代它,并把它传递给需要这些类型的方法。我们将在第十二章展示一些继承自类和特质的单例对象的例子。
类和单例对象间的一个差别是,单例对象不带参数,而类可以。因为你不能用new关键字实例化一个单例对象,你没机会传递给它参数。每个单例对象都被作为由一个静态变量指向的虚构类:synthetic class的一个实例来实现,因此它们与Java静态类有着相同的初始化语法。虚构类的名字是对象名加上一个美元符号。因此单例对象ChecksumAccumulator的虚构类是ChecksumAccumulator$。特别要指出的是,单例对象会在第一次被访问的时候初始化。
不与伴生类共享名称的单例对象被称为孤立对象:standalone object。由于很多种原因你会用到它,包括把相关的功能方法收集在一起,或定义一个Scala应用的入口点。下一段会说明这个用例。
【相关阅读】