Scala语法易混淆点
引用书目
- Programming in Scala 2nd Edition, Martin Odersky
- Programming Scala, Dean Wampler
- Scala Puzzlers, Andrew Philips
以下问题的产生根源都是没有掌握 Scala Language Specification。而Scala语言本身仍在演进,所以未来仍有变化。
配图系用PlantUML编写,博客园无法自动渲染,我又懒得取图,将就这样吧。
类的初始化路径
Scala不支持多重继承,而是改用trait作为类的扩展(混入),这就给继承路径上出现的成员初始化带来了挑战。但任何一个class在其初始化时,都遵循以下原则:
- 成员总是按其声明出现的顺序进行初始化。
- 父类总是优先于子类构造。
- 按主构造子里的语句顺序完成初始化。
trait A {
val audience: String
println("Hello " + audience)
}
trait AfterA {
val introduction: String
println(introduction)
}
abstract class Base(val introduction: String) {
}
class BEvery(val audience: String)
extends Base(introduction = {println("Evaluating early def"); "Are you there?" })
with A
with AfterA {
println("I repeat: Hello " + audience)
}
val b = new BEvery({ println("Evaluating param"); "Readers" })
换个视角,这几乎等价于按当前类的参数声明、继承声明、从左至右的若干父class或trait的构造/初始化、当前类主构造函数的顺序完成初始化。
class BEveryEquivalent(val audience: String = ({ println("Evaluating param"); "Readers" })) {
val introduction = {println("Evaluating early def"); "Are you there?" }
// val audience: String = "Readers"
println("Hello " + audience)
// val introduction: String = "Are you there?"
println(introduction)
println("I repeat: Hello " + audience)
}
另外要特别注意的,是def与val在应用覆写时的差异:
- 所有被覆写的val成员,将在其父class/trait初始化期间保持类型缺省值,直到在子类中覆写时才完成赋值。
- 所有被覆写的无参def函数,因为本就不属于初始化的一部分,所以和lazy val一样,都是在被引用时才返回值。同时,由于父class/trait被覆写,所以会直接调用最新的被覆写后的函数。
trait A {
val foo: Int
val bar = 10
def din = 12
lazy val wah = 5
println("In A: foo: " + foo + ", bar: " + bar + ", din: " + din + ", wah: " + wah)
}
class B extends A {
val foo: Int = 25
println("In A: foo: " + foo + ", bar: " + bar + ", din: " + din + ", wah: " + wah)
}
class C extends B {
override val bar = 99
override def din: Int = 24
override lazy val wah = 30
println("In A: foo: " + foo + ", bar: " + bar + ", din: " + din + ", wah: " + wah)
}
new C()
super调用
class与trait中的super调用,总是沿着一条特定的序列进行查找。这与构造顺序密切相关,它总是与继承声明的顺序相反,超靠后声明继承关系的base class或trait将越早被调用,而这些base class或trait本身在继承树上的父类也都将被纳入这条序列。
trait Animal {
override def toString: String = "I'm animal."
}
trait Feature {
override def toString: String = "It's feature."
}
trait Furry extends Feature {
override def toString: String = "I'm furry. -> " + super.toString
}
trait HasLegs extends Feature {
override def toString: String = "I has legs. -> " + super.toString
}
trait FourLegged extends HasLegs {
override def toString: String = "I has four legs. -> " + super.toString
}
class Cat extends Animal with FourLegged with Furry {
override def toString: String = "I'm a cat. -> " + super.toString
}
val cat = new Cat
构造这个序列的过程,就象每次取继承声明最靠后的class或trait及其父类向列表里追加:
- 彼此有继承关系的,则子类优先于父类。
- 彼此是同宗亲戚时,则先来的优先于后来的。
- 彼此无继承关系时,则新来的将截断旧有的。
- Cat extends Animal
(*) -> Cat
-> Animal #lightblue
-> (*)
- Cat extends Animal with ... with Furry
(*) -> Cat
-> Furry #lightblue
-> Feature #lightblue
-> (*)
Cat -[dashed]-> [被截断] Animal
- Cat extends Animal with FourLegged with Furry
(*) -> Cat
Cat -> Furry
-> FourLegged #lightblue
-> HasLegs #lightblue
-> Feature
-> (*)
Cat -[dashed]-> [被截断] Animal
会反复尝试的lazy val
lazy val会反复尝试初始化,直到其成功获取赋值。(这一特性可用于反复争取某个资源。)
def trylazy = {
var x = 0
lazy val y = 1 / x
try {
println(y)
} catch {
case _: Exception =>
x = 1
println(y)
}
}
trylazy
老实巴交的map
map在映射时不会改动原来的Collection元素顺序,于是使用未经过中间迭代转换的Collection便会得到意料之外的的排列顺序。
case class RomanNumeral(symbol: String, value: Int)
implicit object RomanOrdering extends Ordering[RomanNumeral] {
def compare(a: RomanNumeral, b: RomanNumeral) =
a.value compare b.value
}
val numerals = SortedSet(
RomanNumeral("M", 1000),
RomanNumeral("C", 100),
RomanNumeral("X", 10),
RomanNumeral("I", 1),
RomanNumeral("D", 500),
RomanNumeral("L", 50),
RomanNumeral("V", 5)
)
println("Roman numeral symbols for 1 5 10 50 100 500 1000:")
for (num <- numerals; sym = num.symbol) { print(s"${sym} ") }
numerals map { _.symbol } foreach { sym => print(s"${sym} ") }
numerals.view map { _.symbol } foreach { sym => print(s"${sym} ") }
转载请注明出处及作者,谢谢!