Parameterized Types: Variance Under Inheritance
Java和Scala在类型化参数(泛型)上最大的不同是variance under inheritance(继承下的变型)是怎么工作的.
比如一个方法接收一个List[AnyRef]类型的参数,你能给它传一个List[String]吗,或者说 List[String]是List[AnyRef]的子类型吗?如果是,则这种型变就叫做协变(covariance),因为容器的父子类型关系和类型参数的关系走势一致.
同时还有逆变(contravariant),即 X[String] 是 X[Any] 的父类型.
如果一个参数化类型就不是协变也不是逆变,则称为不可变型(invariant).
Java和scala均支持协变,逆变与不可变型,但是在Scala中,变型行为通过叫做变型注解(variance annotation)的方式在每个参数上作为类型声明的一部分被定义.
协变时使用符号 +, 逆变时使用符号 -,不变型时不用添加符号,下面是一些声明的例子:
class W[+A] {...} // covariant
class X[-A] {...} // contravariant
class Y[A] {...} // invariant
class Z[-A,B,+C] {...} // mixed
类型变型注解和意义:
| Scala | Java | Description |
|---|---|---|
| +T | ? extends T | 协变(eg:List[Tsub]是List[T]的子类型) |
| -T | ? super T | 逆变(eg:X[Tsup]是X[T]的子类型) |
| T | T | 不可变型(eg:不能将Y[Tsub]或Y[Tsup]适应为Y[T]) |
这时候再看List,实际上声明为List[+A],即List[String]是List[Any]的子类型,因此List在参数A是进行协变.
如果一个类型跟List一样只有一种变型,则说”List是协变的”,逆变也适用这种说法.
Functions Under the Hood
逆变最好的例子是一组FunctionN的特质,比如Scala.Function2,N是区间是0-22,根据函数的参数数量决定.Scala根据这些特质实现匿名函数.
我们已经见过匿名函数:
List(1,2,3,4)map(i=>i+3) // Result: List(4, 5, 6, 7)
函数表达式 i => i + 3 实质上是一个语法糖,编译器会把它转换成scala.Function1的匿名子类实例:
val f: Int => Int = new Function1[Int,Int] {
def apply(i: Int): Int = i + 3
}
List(1, 2, 3, 4) map (f) // Result: List(4, 5, 6, 7)
FunctionN特质是抽象的,因为其apply方法是抽象的.注意我们这里已经定义了他,表达式 i => i + 3 的作用就是用于定义apply方法.
现在回到逆变,如下是scala.Function2的声明:
trait Function2[-T1, -T2, +R] extends AnyRef
最后一个类型参数 +R 是返回值类型.它是协变的.前面的两个类型参数分别是函数参数.他们你逆变的,其他的FunctionN特质于此类似.
观察实例理解变型行为:
class CSuper { def msuper() = println("CSuper") } // 1
class C extends CSuper { def m() = println("C") }
class CSub extends C { def msub() = println("CSub") }
var f: C => C = (c:C) =>newC // 2
f = (c: CSuper) => new CSub // 3
f = (c: CSuper) =>new C // 4
f = (c:C) =>newCSub // 5
f = (c: CSub) => new CSuper // 6 COMPILATION ERROR!
- 定义一组三重继承的类
- 把函数 f 定义为 var 以便能够重新赋值,所有进行赋值的函数实例必须是 C => C(Function1[C,C]),所有我们赋值的函数必须能够满足继承下的变型要求.
- 函数值 (c: CSuper) => new CSub 是有效的,因为参数C是逆变,所以CSuper是一个有效的替换,同时返回值是协变的,因此CSub可以有效替换C
- 和上面的一样,只是返回了一个 C
- 和上面一样,只是传入一个 C
- 错误,接收一个CSub参数是无效的,因为逆变. 返回值类型CSuper对于协变也是无效的
协变时可以使用子类进行替换,逆变时可以使用父类来替换.
函数变量 f 是一个 C => C (Function1[-C,+C])类型,第一次赋值跟签名准确吻合,即传入值与传出值均是C类型.
第二次赋值,(x:CSuper) => CSub,同样遵守了逆变参数和协变返回值,但是为什么这样就安全(可用)呢?
关键的洞擦力是 f 函数具体是如何使用的或者我们可以使用它完成什么功能.如果我们定义为 C => C,则所有有效的C类型值可以传入同时永远不会返回C类型意外任何类型的值.
但如果实际类型是 (x:CSuper) => CSub, 该函数就不仅仅只能接受C类型的值,它同时能够接收其父类型CSuper或它其他的子类型的值. 因此我们只会传入一个C的实例,而不会传入任何超出 f 函数允许范围之外的类型.而此时,f 的能力比他需要做的要宽大的多.
同样,如果他只返回CSub实例也是同样安全的,因为调用者既然能够处理C,当然能够处理其子类CSub.
上面的第6项同时违背了两条规则,我们观察一下如果我们真的允许这么做了会发生什么.
这个例子中,函数 f 只知道怎么处理CSub的实例,但是调用者以为任何C的实例都能够传入,因此运行时会报错,比如调用一些仅仅在CSub中定义的方法,而C中没有.同样,如果 f 能够返回一个CSuper,这同样会让调用者吃惊,因为返回值超出了预期的实例类型,而预期的是类型C的实例.
这就是为什么函数的参数必须逆变而返回值必须协变.
变型注解只能作用于类的类型参数,不能用作参数化方法,因为注解影响了子类型的行为.方法并不是类型化的.比如List.map方法的签名:
sealed abstract class List[+A] ... { // mixin traits omitted ...
def map[B](f: A => B): List[B] = {...}
...
}
B上并没有类型注解,如果强加一个则会编译器报错.
最后编译器会检查你变型注解中的无效用法,比如下面在函数定义中使用了错误的注解:
scala> trait MyFunction2[+T1, +T2, -R] {
| defapply(v1:T1,v2:T2):R=???
|}
<console>:37: error: contravariant type R occurs in covariant position
in type (v1: T1, v2: T2)R of method apply
def apply(v1:T1, v2:T2): R = ???
^
<console>:37: error: covariant type T1 occurs in contravariant position
in type T1 of value v1
def apply(v1:T1, v2:T2): R = ???
^
<console>:37: error: covariant type T2 occurs in contravariant position
in type T2 of value v2
def apply(v1:T1, v2:T2): R = ???
^
注意看到错误提示中,编译器强制要求函数参数逆变,返回值协变.
Variance of Mutable Types
目前为止我们讨论的参数化类型都是不可变的.那可变类型的变型行为又是怎样的呢.答案就是只允许 不可变型:
scala> class ContainerPlus[+A](var value: A)
<console>:34: error: covariant type A occurs in contravariant position
in type A of value value_=
class ContainerPlus[+A](var value: A)
^
scala> class ContainerMinus[-A](var value: A)
<console>:34: error: contravariant type A occurs in covariant position
in type => A of method value
class ContainerMinus[-A](var value: A)
^
可变字段的问题是,它的行为像是一个带有公共读写方法的私有字段,甚至是一个没有明显访问方法的共有字段.
def value_ = (newA: A): Unit+ 是编译器解释后的变量的setter签名,意思是,当我们写一个 myinstance.value = someA 这样的表达式时这个方法就会调用,因此注意第一条错误提示,我们在逆变的位置使用了协变类型A.
The Scala Type Hierarchy
参考Scala标准库类型继承图.
在分层结构的顶层是Any,它没有父类,有三个子类:
- AnyVal,类型值(value types)和类值(value classes)的父类
- AnyRef,所有引用类型的父类
- 通用特质
AnyVal有一个具体的子类叫做类型值(value types),有七种数字值类型:Byte, Char, Short, Int, Long, Float, Double,剩余的两个非数字类型是Unit和Boolean.
而其他所有类型都是引用类型(reference types),都是从AnyRef派生,与Java中的java.lang.Object同源.
Much Ado About Nothing (and Null)
处于在类型系统底层的Nothing和Null是两个特殊的类型,比较特殊的是Nothing是其他所有值类型的子类型,而Null是所有引用类型的子类型.
Null是很多编程语言中常用的概念,但是通常使用的时候不是定位为Null类型,只是用一个关键字null付给一个引用以表明该引用没有实际的值,其源码实现如下:
package scala
abstract final class Null extends AnyRef
为什么它能够同时是abstract和final呢? 该声明不允许有子类或创建自己的实例,运行时环境会提供一个实例.
Nothing在Java中没有对应的实现,Nothing的实现:
package scala
abstract final class Nothing extends Any
Nothing实际上继承自Any,因此Nothing是其他所有类型的子类型,所有的引用类型或值类型.
Products, Case Classes, and Tuples
case类混入了scala.Product特质,为实例提供一些操作字段的泛型方法,比如一个Person实例:
scala> case class Person(name: String, age: Int)
defined class Person
scala> val p: Product = Person("Dean", 29)
p: Product = Person(Dean,29) //The case class instance is assignable to a Product variable.
scala> p.productArity
res0: Int = 2 //The number of fields.
scala> p.productElement(0)
res1: Any = Dean //Elements counted from zero.
scala> p.productElement(1)
res1: Any = 29
scala> p.productIterator foreach println
Dean
29
使用泛型方法访问字段是非常有用的,他们的值类型被限定为Any而不是他们实际的类型.
Product同时有一些子类用于专门的用途,这些类型添加了一些方法用于选择字段,比如 Product2[+T1,+T2] 添加的方法:
package scala
trait Product2[+T1, +T2] extends Product {
abstract def _1: T1
abstract def _2: T2 ...
}
这些方法会返回字段的实际类型,类型参数均为协变,因为特质ProductN只被用于不可变类型.
这些方法的调用和访问元组元素的方法一致,实际上TupleN继承了ProductN并为从 _1到 _N的方法提供了具体的实现:
scala> val t2 = ("Dean", 29)
t2: (String, Int) = (Dean,29)
scala> t2._1
res0: String = Dean
scala> t2._2
res2: Int = 29
scala> t2._3
<console>:36: error: value _3 is not a member of (String, Int)
t2._3
^
Tuple2中并没有实现 _3这个方法.
The Predef Object
编译器自动导入Predef定义已提供一些有用的定义.
隐式转换
首先Predef提供了很多隐式转换,一组封装了AnyVal类型的转换:
@inline implicit def byteWrapper(x: Byte)
@inline implicit def shortWrapper(x: Short)
@inline implicit def intWrapper(x: Int)
@inline implicit def charWrapper(c: Char)
@inline implicit def longWrapper(x: Long)
@inline implicit def floatWrapper(x: Float)
@inline implicitdefdoubleWrapper(x:Double)
@inline implicit def booleanWrapper(x: Boolean) = new runtime.RichBoolean(x)
这些 Rich* 类型提供了额外的方法,比如比较方法 <= 和 compare,@inline 注释提醒编译器”内联”这些方法调用.
为什么不把这些方法都放到Byte内而是有两种版本的Byte呢,原因由于字节码的执行需要,这些方法需要在堆中分配一个实例.但是Byte实例,还有其他的AnyVal类型并不分配在堆中,而是像Java中的基本类型一样放在栈中.因此只有在需要这些方法时才会将其放到堆中.
同样对Java的可变数组即scala.col lection.mutable.WrappedArray的实例进行了方法封装,添加了很多集合方法:
implicit def wrapIntArray(xs: Array[Int]): WrappedArray[Int]
implicit def wrapDoubleArray(xs: Array[Double]): WrappedArray[Double]
implicit def wrapLongArray(xs: Array[Long]): WrappedArray[Long]
implicit def wrapFloatArray(xs: Array[Float]): WrappedArray[Float]
implicit def wrapCharArray(xs: Array[Char]): WrappedArray[Char]
implicit def wrapByteArray(xs: Array[Byte]): WrappedArray[Byte]
implicit def wrapShortArray(xs: Array[Short]): WrappedArray[Short]
implicit def wrapBooleanArray(xs: Array[Boolean]): WrappedArray[Boolean]
implicit def wrapUnitArray(xs: Array[Unit]): WrappedArray[Unit]
为何将每种AnyVal类型的方法都分开了呢? 容器元素的基本类型比数组的执行效率更高,因此为了提高效率,避免使用引用类型的泛型实现.
同样提供了java基本类型的封装与scala中AnyVal之间的转换:
implicit def byte2Byte(x: Byte) = java.lang.Byte.valueOf(x)
implicit def short2Short(x: Short) = java.lang.Short.valueOf(x)
implicit def char2Character(x: Char) = java.lang.Char.valueOf(x)
implicit def int2Integer(x: Int) = java.lang.Int.valueOf(x)
implicit def long2Long(x: Long) = java.lang.Long.valueOf(x)
implicit def float2Float(x: Float) = java.lang.Float.valueOf(x)
implicit def double2Double(x: Double) = java.lang.Double.valueOf(x)
implicit def boolean2Boolean(x: Boolean) = java.lang.Boolean.valueOf(x)
implicit def Byte2byte(x: java.lang.Byte): Byte = x.byteValue
implicit def Short2short(x: java.lang.Short): Short = x.shortValue
implicitdefCharacter2char(x:java.lang.Character):Char = x.charValue
implicit def Integer2int(x: java.lang.Integer): Int = x.integerValue
implicit def Long2long(x: java.lang.Long): Long = x.longValue
implicit def Float2float(x: java.lang.Float): Float = x.floatValue
implicit def Double2double(x: java.lang.Double): Double = x.doubleValue
implicit def Boolean2boolean(x: java.lang.Boolean): Boolean = x.booleanValue
Type Definitions
Predef中定义了一些类型和类型别名.
我了促进不可变集合的使用,Prefef中定义了一些常用不可集合类型的别名:
type Map[A, +B] = collection.immutable.Map[A, B]
type Set[A] = collection.immutable.Set[A] type
Function[-A, +B] = Function1[A, B]
对于量元素和三元素元组的两个方便的别名:
type Pair[+A, +B] = Tuple2[A, B]
type Triple[+A, +B, +C] = Tuple3[A, B, C]
Condition Checking Methods
有时候需要断言一个条件为真,或者在测试时需要快速失败,Predef为此提供了一些方法:
测试断言为真,否则抛出java.lang.AssertionError.
def assert(assertion: Boolean)
与上面类似,同时携带一个参数并转换为String:
def assert(assertion: Boolean, message: => Any)
和assert类似,但是传达的意思是,当进入一个代码段,如一个方法时,该条件被假定为真的.
def assume(assertion: Boolean, message: => Any)
def require(requirement: Boolean)
def require(requirement: Boolean, message: => Any)
输入输入方法
Miscellaneous Methods
未实现方法:
def ???: Nothing
简单的返回参数x:
def identity[A](x: A): A
当一个隐式参数列表指定简写为[T : M],这时,编译器将隐式的添加隐式参数列表(implicit arg: M[T]):
def implicitly[T](implicit e: T): T
Equality of Objects
The equals Method
我们使用case类来说明比较方法的不同.
case class Person(firstName: String, lastName: String, age: Int)
val p1a = Person("Dean", "Wampler", 29)
val p1b = Person("Dean", "Wampler", 29)
val p2 = Person("Buck", "Trends", 30)
equals方法进行的是值比较,二者不需要是同一个实例:
p1a equals p1a // = true
p1a equals p1b // = true
p1a equals p2 // = false
p1a equals null // = false
null equals p1a // throws java.lang.NullPointerException
null equals null // throws java.lang.NullPointerException
The == and != Methods
同样是值比较,唯一例外的是左侧为null时:
p1a == null // = false
null == p1a // = false
null == null // = true (compiler warns that it's always true)
!= 等同于 !(obj1 == obj2).
The eq and ne Methods
eq 用于引用比较,即二者在内存中的位置一致,仅被AnyRef定义:
p1a eq p1a // = true
p1a eq p1b // = false
p1a eq p2 // = false
p1a eq null // = false
null eq p1a // = false
null eq null // = true (compiler warns that it's always true.)
ne 等同于 !(obj1 eq obj2).
Array Equality and the sameElements Method
对两个数组的比较不能得到一个明显的结果:
Array(1, 2) == Array(1, 2) // = false
但是有一个有效的方法,即 sameElements:
Array(1, 2) sameElements Array(1, 2) // = true
但是根据Java的特性,Array是可变的,因此不能有效的进行比较,若果需要,考虑能不能换成序列,比如List:
List(1, 2) == List(1, 2) // = true
List(1, 2) sameElements List(1, 2) // = true
Overriding Members of Classes and Traits
类和特质可以声明抽象的字段,方法或类型,这些程序员在创建实例前必须被其派生类完整定义.
覆写成员时Scala强制需要override关键字,但是在子类定义抽象成员时为可选.即子类中该成员仍然未被实现时,添加override关键字是错误的.
使用override关键字的一些好处:
- 编译器会检查哪些已被标记为override的成员定义,如果没有任何重写则会抛出错误.
- 如果在基类中添加了一个新成员,而该成员name与派生类中成员name重复时会报错.
- 添加override关键字会提醒你考虑哪些成员需要重写而那些不需要.
Avoid Overriding Concrete Members: 避免重写实体成员
case class Address(city: String, state: String, zip: String)
case class Employee(name: String, salary: Double, address: Address)
abstract class Payroll {
def netPay(employee: Employee): Double = { // 1
valfedTaxes =calcFedTaxes(employee.salary)
val stateTaxes = calcStateTaxes(employee.salary, employee.address)
employee.salary - fedTaxes -stateTaxes
}
def calcFedTaxes(salary: Double): Double // 2
def calcStateTaxes(salary: Double, address: Address): Double // 3
}
object Payroll2014 extends Payroll {
val stateRate = Map(
"XX" -> 0.05,
"YY" -> 0.03,
"ZZ" -> 0.0)
def calcFedTaxes(salary: Double): Double = salary * 0.25 // 4
def calcStateTaxes(salary: Double, address: Address): Double = {
// Assume the address.state is valid; it's found in the map!
salary * stateRate(address.state)
}
}
val tom = Employee("Tom Jones", 100000.0, Address("MyTown", "XX", "12345"))
val jane = Employee("Jane Doe", 110000.0, Address("BigCity", "YY", "67890"))
Payroll2014.netPay(tom) // Result: 70000.0
Payroll2014.netPay(jane) // Result: 79200.0
- 方法netPay使用了模板方法模式,它定义了计算薪水的协议
- 抽象方法用于计算联邦税
- 抽象方法用于计算州税
- 实现父类中定义的抽象方法
上面没有使用到任何override关键字.
尽量避免重写父类实体成员,除了toString.不要使用override关键字除非你真的在重写一个实体成员.
Attempting to Override final Declarations
如果一个成员被final关键字生面,则该成员被禁止重写.
Overriding Abstract and Concrete Methods
定义一个抽象类:
abstract class Widget {
def draw(): Unit
override def toString() = "(widget)"
}
因为draw并没有实体,它是一个抽象方法,同时Widget也被声明为abstract.任何一个Widget的子类必须实现draw方法或派生自一个实现该方法的父类.
因为所以的AnyRef都已经定义了toString方法,因此这时override关键字是必须的.
下面是一个派生类:
import progscala2.traits.ui2.Clickable
class Button(val label: String) extends Widget with Clickable {
// Simple hack for demonstration purposes:
def draw(): Unit = println(s"Drawing: $this")
// From Clickable:
protected def updateUI(): Unit = println(s"$this clicked; updating UI")
override def toString() = s"(button: label=$label, ${super.toString()})"
}
Overriding Abstract and Concrete Fields
Overriding fields in traits
trait AbstractT2 {
println("In AbstractT2:")
val value: Int
val inverse = 1.0/value // 当inverse被初始化时,value的值是多少呢
println("AbstractT2: value = "+value+", inverse = "+inverse)
}
val obj = new AbstractT2 {
println("In obj:")
val value = 10
}
println("obj.value = "+obj.value+", inverse = "+obj.inverse)
当我们通过一个匿名类继承该特质并尝试运行以上代码时:
In AbstractT2:
AbstractT2: value = 0, inverse = Infinity
In obj:
obj.value = 10, inverse = Infinity
inverse 的值被计算的过早,因为value并没有被实现.
Scala提供了两种方法来解决这个问题.
trait AbstractT2 {
println("In AbstractT2:")
val value: Int
lazy val inverse = 1.0/value // 添加lazy关键字并注释表println
// println("AbstractT2: value = "+value+", inverse = "+inverse)
}
val obj = new AbstractT2 {
println("In obj:")
val value = 10
}
println("obj.value = "+obj.value+", inverse = "+obj.inverse)
这时候inverse被正确求值:
In AbstractT2:
In obj:
obj.value = 10, inverse = 0.1
然后,lazy只有在println被注释时有用,如果取消对println的注释,inverse又会被计算为Infinity,因为lazy只有在第一次使用时进行求值.println则对inverse的使用过早.
If a val is lazy, make sure all uses of the val are also as lazy as possible.
另一种解决方式,预初始化字段:
trait AbstractT2 {
println("In AbstractT2:")
val value: Int
val inverse = 1.0/value
println("AbstractT2: value = "+value+", inverse = "+inverse)
}
val obj=new{
// println("In obj:") // 1
val value = 10
} with AbstractT2
println("obj.value = "+obj.value+", inverse = "+obj.inverse)
- 预初始化块 只允许 类型定义 和 实体字段定义.如果使用println则会报错.
这时重新运行则会成功求值:
In AbstractT2:
AbstractT2: value = 10, inverse = 0.1
obj.value = 10, inverse = 0.1
在trait中inverse就已经被求值了.
然后看一个VetoableClicks特质,定义了一个maxAllowed初始化为1:
trait VetoableClicks extends Clickable { // 1
// 允许被点击的默认值
val maxAllowed = 1 // 2
private var count = 0
abstract override def click() = {
if (count < maxAllowed) { // 3
count += 1
super.click() }
}
}
}
- 继承Clickable
- 允许的最大点击数
- 一旦点击数超过了允许值,不会有更多的点击发送给super
让我们看一下使用中遇到的问题:
trait ObservableClicks extends Clickable with Subject[Clickable] {
abstract override def click(): Unit = { // 1
super.click()
notifyObservers(this)
}
}
- 注意关键字 abstract override
然后进行测试:
val observableButton = // 1
new Button("Okay") with ObservableClicks with VetoableClicks {
override val maxAllowed: Int = 2 // 2
}
assert(observableButton.maxAllowed == 2, s"maxAllowed = ${observableButton.maxAllowed}") // 3
class ClickCountObserver extends Observer[Clickable] { // 4
var count = 0
def receiveUpdate(state: Clickable): Unit = count += 1
}
val clickCountObserver = new ClickCountObserver // 5
observableButton.addObserver(clickCountObserver)
val n = 5 // 6
for (i <- 1 to n)
observableButton.click()
assert(clickCountObserver.count == 2, s"count = ${clickCountObserver.count}. Should be != $n") // 7
- 通过需要的特质构造一个button
- 重写特质中的val字段,重写时必须提供完整的定义
- 经验证,成功修改了maxAllowed的值
- 定义一个管擦者类以记录点击数
- 实例化一个观察者并注册到button
- 点击button 5 次
- 验证管擦者只收到了两次点击,而其他的点击被拒绝了
上面已经看到了重写val字段的定义.如果maxAllowed被声明为var,该如何重写呢:
val observableButton =
new Button("Okay") with ObservableClicks with VetoableClicks {
maxAllowed = 2
}
重写可变字段时,不再需要override关键字和完整的签名,比如var 和类型.
Overriding fields in classes
在class中重写val和var:
class C1 {
val name = "C1"
var count = 0
}
class ClassWithC1 extends C1 {
override val name = "ClassWithC1"
count = 1
}
val c = new ClassWithC1()
println(c.name)
println(c.count)
重写抽象类型的val和var:
abstract class AbstractC1 {
val name: String
var count: Int
}
class ClassWithAbstractC1 extends AbstractC1 {
val name = "ClassWithAbstractC1"
var count = 1
}
val c = new ClassWithAbstractC1()
println(c.name)
println(c.count)
重写抽象的val时不再需要override关键字.
Overriding Abstract Types
一个BulkReader的例子:
abstract class BulkReader {
type In
val source: In
def read: String // Read source and return a String
}
class StringBulkReader(val source: String) extends BulkReader {
type In = String
def read: String = source
}
...
上面的例子中展示了如何定义一个抽象类型,以及如何在派生类中实现一个抽象类型.
与字段方法不同,不能够重写一个已经定义的类型.
Linearization of an Object’s Hierarchy
由于单继承的原因,如果我们混入多个特质,则多重继承会表现为线性形式.
下面是一个实例:
class C1 {
def m = print("C1 ")
}
trait T1 extends C1 {
override def m = { print("T1 "); super.m }
}
trait T2 extends C1 {
override def m = { print("T2 "); super.m }
}
trait T3 extends C1 {
override def m = { print("T3 "); super.m }
}
class C2 extends T1 with T2 with T3 {
override def m = { print("C2 "); super.m }
val c2 = new C2
c2.m
运行结果:
C2 T3 T2 T1 C1
可以发现,特质中的m方法会根据声明中的顺序从左到右依次调用,然后在最后调用到C1.
然后我们观察构造函数的调用顺序:
class C1 {
print("C1 ")
}
trait T1 extends C1 {
print("T1 ")
}
trait T2 extends C1 {
print("T2 ")
}
trait T3 extends C1 {
print("T3 ")
}
class C2 extends T1 with T2 with T3 {
println("C2 ")
}
val c2 = new C2
// C1 T1 T2 T3 C2
构造函数的顺序是反向的. 这个调用顺序是有意义的,因为派生类的构造会用到父类中的成员,因此父类必须在派生类构造之前完成自身的构造.
上面第一个例子中在最后省略了两个类型,引用类型的完整线性化其实是以AnyRef和Any结束,因此C2的线性化应该是这样:
C2 T3 T2 T1 C1 AnyRef Any
另外,值类型,所有AnyVal的子类,都被声明为abstract final,编译器对他们的初始化进行管理.因为我们不能从他们派生类,他们的线性化也是简单直接的.
值类(value classe)又是怎样的呢? 如下实例中,值类并不允许我们在类型体内添加打印语句:
class USPhoneNumber(val s: String) extends AnyVal {
override def toString = {
val digs = digits(s)
val areaCode = digs.substring(0,3)
val exchange = digs.substring(3,6)
val subnumber = digs.substring(6,10) // "subscriber number"
s"($areaCode) $exchange-$subnumber"
}
private def digits(str: String): String = str.replaceAll("""\D""", "")
}
val number = new USPhoneNumber("987-654-3210")
// Result: number: USPhoneNumber = (987) 654-3210
当调用m时会打印以下结果:
USPhoneNumber Formatter Digitizer M