原文链接 http://nerd-is.in/2013-08/scala-learning-traits/
原文发表于http://nerd-is.in/2013-08/scala-learning-traits/
Scala特质可以给出特质的缺省实现
不支持多重继承
Scala也还是不支持多重继承。
如果几个类有某些共通的方法或者字段,那么从它们多重继承时,
就会出现麻烦。所以Java被设计成不支持多重继承,但可实现任意多的接口。
接口只能包含抽象方法,不能包含字段。
而Scala中的特质,可以同时拥有抽象方法和具体方法,类可以实现多个特质。
当做接口使用的特质
1 2 3 4 5 6 7 8 9 10 |
trait Logger { def log(msg: String) } class ConsoleLogger extends Logger { def log(msg: String) { println(msg) } } // 使用with添加额外的特质 class ConsoleLogger extends Logger with Cloneable with Serializable |
所有的Java接口都可以作为Scala特质来使用。
与Java一样,Scala类只能有一个超类,可以有任意数量的特质。
在解读时,Logger with Cloneable with Serializable先被当成一个整体,然后类extends这个整体。
带有具体实现的特质
特质中的方法不需要一定是抽象的,可以有具体的实现。
1 2 3 4 5 6 7 8 9 10 11 |
trait ConsoleLogger { def log(msg: String) { println(msg) } } class SavingsAccount extends Account with ConsoleLogger { def withdraw(amount: Double) { if (amount > balance) log("Insufficient funds") else balance -= balance } ... } |
在Scala中,我们说ConsoleLogger的功能被混入了SavingsAccount类。
特质拥有具体行为的一个弊端是,当特质改变时,所有混入了该特质的类都必须重新编译。
带有特质的对象
在构造对象时,可以混入该对象所具有的特质的子类型。
那么在调用这个对象所具有的特质方法时,将会执行子类型的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
trait Logged { def log(msg: String) { } } class SavingsAccount extends Account with Logged { def withdraw(amount: Double) { if (amount > balance) log("Insufficient funds") else ... } ... } trait ConsoleLogger extends Logged { override def log(msg: String) { println(msg) } } val acct = new SavingsAccount with ConsoleLogger // 当调用acct的log方法时,执行的是ConsoleLogger特质的log方法 |
叠加在一起的特质
可以为类或对象添加多个相互调用的特质,调用将会从最后一个特质开始。
这个功能对需要分阶段加工处理某个值的场景很有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// 给日志消息加上时间 trait TimestampLogger extends Logged { override def log (msg: String) { super.log(new java.util.Date() + " " + msg) } } // 截断过长的的日志 trait ShortLogger extends Logged { val maxLength = 15 override def log(msg: String) { super.log( if (msg.length <= maxLength) msg else msg.substring(0, maxLength - 3) + "...") } } val acct1 = new SavingsAccount with ConsoleLogger with TimestampLogger with ShortLogger val acct2 = new SavingsAccount with ConsoleLogger with ShortLogger with TimestampLogger |
上面定义的两个特质中,都调用了super.log。这个super.log与类中的含义并不一样,
不是调用了父级的方法,而是调用了特质层级中的下一个特质。具体是调用了哪一个特质,
是根据特质的添加顺序来决定的。一般来说,特质从最后一个开始被处理。
后面会说明当特质的顺序不是简单的链而是任意形态的树/图时的细节(在这里书中用的形容词是血淋淋…)。
如果从acct1取款,首先调用的ShortLogger的log方法,由于方法中调用了super.log,
于是又去调用下一个特质的log方法(也就是TimestampLogger的log)。
于是输出结果会是完整的时间,以及不完整的msg(被ShortLogger截断了)。
那么如果从acct2取款,顺序将会不一样,最终输出的结果会是不完整的时间。
如果要控制特定特质的方法被调用,可以在方括号中给出特质或类的名称。
这里给出的名称,必须是直接的超类型。 super[ConsoleLogger].log(...)
在特质中重写抽象方法
1 2 3 |
trait Logger { def log(msg: String) } |
现Logger特质中有一个抽象方法log,如果我们用原来的TimestampLogger特质,会无法编译。
因为Logger的log方法没有具体的实现,Scala会认为TimestampLogger依旧是抽象的。
于是我们需要加上abstract和override关键字:
1 2 3 4 5 |
trait TimestampLogger extends Logger { abstract override def log(msg: String) { super.log(new java.util.Date() + " " + msg) } } |
当做富接口使用的特质
在Scala中,在特质中混用具体方法和抽象方法十分普遍;
而Java中需要声明一个接口和一个额外的扩展该接口的类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
trait Logger { def log(msg: String) def info(msg: String) { Log("INFO: " + msg) } def warn(msg: String) { Log("WARN: " + msg) } def severe(msg: String) { log("SERVERE: " + msg) } } class SavingsAccount extends Account with Logger { def withdraw(amount: Double) { if (amount > balance) severe("Insufficient funds") else ... } ... override def log(msg: String) { println(msg) } } |
特质中的具体字段
特质中的字段可以是具体的,也可以是抽象的。如果给出了初始值,字段就是具体的。
混入了该特质的类会自动获得特质的具体字段,但这些字段不是被继承的,
而是被简单加入到了子类中。这之间是有差别的。
从特质中加入的具体字段,在内存的模型上,是不属于继承的超类对象的,
是属于子类本身的。表述不太清楚,不过看书都应该能明白。
特质中的抽象字段
特质的抽象字段必须在具体的子类中进行重写。
特质构造顺序
特质也可以有构造器,由字段的初始化和其他特质体中的语句构成。
这些语句在任何混入该特质的对象在构造是都会被执行。
构造器的执行顺序:
- 调用超类的构造器;
- 特质构造器在超类构造器之后、类构造器之前执行;
- 特质由左到右被构造;
- 每个特质当中,父特质先被构造;
- 如果多个特质共有一个父特质,父特质不会被重复构造
- 所有特质被构造完毕,子类被构造。
构造器的顺序是类的线性化的反向。线性化是描述某个类型的所有超类型的一种技术规格。
初始化特质中的字段
特质不能有构造器参数,每个特质有一个无参数的构造器。
对于需要某种定制才有用的特质来说,这个局限是一个问题。
用文件日志生成器来说明,我们需要在使用特质时指定日志文件,但是特质不能使用构造参数。
可以考虑使用抽象字段来存放文件名:
1 2 3 4 5 6 7 8 9 |
trait FileLogger extends Logger { val filename: String val out = new PrintStream(filename) def log(msg: String) { out.println(msg); out.flush() } } val acct = new SavingsAccount with FileLogger { val filename = "myapp.log" } |
但是这样却是行不通的。问题来自于构造顺序。FileLogger的构造器会先于子类构造器执行,
这里的子类是一个扩展了SavingsAccount且混入了FileLogger的匿名类实例。
在构造FileLogger时,就会抛出一个空指针异常,子类的构造器根本就不会执行。
这个问题的解决方法之一是使用提前定义这个语法(就是前面章节里提过的难看到家了的那个)。
1 2 3 4 5 6 7 8 9 10 11 |
// 使用提前定义,请注意这里的奇怪语法 val acct = new { val filename = "myapp.log" } with SavingsAccount with FileLogger // 在类中使用提前定义 class SavingsAccount extends { val filename = "myapp.log" } with Account with FileLogger { ... } |
另外一个方法是使用懒值:
1 2 3 4 5 |
trait FileLogger extends Logger { val filename: String lazy val out = new PrintStream(filename) def log(msg: String) { out.println(msg) } } |
因为懒值在初次使用是才被初始化,所以out字段不会再抛出空指针异常。
在使用out字段时,filename也已经初始化了。
但是使用懒值不高效。
扩展类的特质
特质可以扩展类,这个类会自动成为所有混入该特质的类的超类。
1 2 3 4 5 6 7 |
trait LoggedException extends Exception with Logged { def log() { log(getMessage()) } } class UnhappyException extends LoggedException { override def getMessage() = "arggh!" } |
特质的超类Exception自动成为了混入了LoggedException特质的UnhappyException的超类。
Scala并不允许多继承。那么这样一来,如果UnhappyException原先已经扩展了一个类了该如何处理?
只要已经扩展的类是特质超类的一个子类就可以。
1 2 |
class UnhappyException extends IOException with LoggedException // 可行 class UnhappyFrame extends JFrame with LoggedException // 不可行 |
自身类型(L2)
当特质扩展类时,编译器能确保所有混入该特质的类都将这个被特质扩展的类作为超类。
除了这一个手段,Scala还可以用自身类型(self type)来确保这一点。
如果特质以 this: type =>开始定义(像在嵌套类一节中使用的用来指定外部类别名的语法),那么这个特质就只能被混入type指定的类型的子类。
1 2 3 4 |
trait LoggedException extends Logged { this: Exception => def log() { log(getMessage()) } } |
这里的特质LoggedException并不扩展Exception类,而是自身拥有Exception类型,
意味着该特质只能被混入Exception的子类。
这样指定了自身类型之后,调用自身类型的方法就合法了(这里调用了Exception类的getMessage方法)。
相比而言,扩展类的特质和拥有自身类型的特质这两者很相似,
但某些情况下自身类型的写法比扩展类版本更灵活。自身类型可以解决特质间的循环依赖。
自身类型还可以处理结构类型(structural type)——这种类型只给出了类必须拥有的方法,而不是类的名称。
1 2 3 4 |
trait LoggedException extends Logged { this: ( def getMessage(): String) => def log() { log(getMessage()) } } |
自身类型和结构类型会在书的18章详细介绍。
背后发生的
了解是如何将特质翻译成JVM的类和接口,有助于理解特质。
如果特质只有抽象方法,特质被转变成一个Java接口。
如果特质具有具体方法,Scala会创建一个伴生类,伴生类用静态方法存放特质的方法。
特质中的字段对应到接口中的抽象getter/setter,
当某个类实现特质时,字段被自动加入。
1 2 3 4 5 6 7 8 9 10 11 |
trait ShortLogger extends Logger { val maxLength = 15 ... } // 翻译成 public interface ShortLogger extends Logger { public abstract int maxLength(); public abstract void weird_prefix$maxLength_$eq(int); ... } |
以weird开头的方法将会在伴生类的初始化方法$init$内被用来初始化字段。
这是《快学Scala》的第10章,内容是跟面向对象比较相关的特质,
但是内容已经不是随便学学玩玩的了。学Scala果然还是挺有挑战性的,
怪不得时常见到说Scala不好学太学术之类的论调。后面还有不少内容,
而且还包括自己以前没接触过的函数式编程范式,走一步看一步吧。
很多细节现在学了也不知道会在什么地方实用到,这么想来,
果然要了解设计的目的是很重要的,至少会明白适用在哪里。
本章练习参考。
1.
1 2 3 4 5 |
trait RectangleLike { this: java.awt.geom.Ellipse2D.Double => def translate(dx: Int, dy: Int) { this.x += dx; this.y += dy } def grow(h: Int, v: Int) { this.height += h; this.width += v } } |
2.
1 2 3 4 5 6 7 8 |
class OrderedPoint extends java.awt.OrderedPoint with scala.math.Ordered[java.awt.OrderedPoint] { def compare(that: java.awt.OrderedPoint) = { if ((this.x < that.x) || (this.x == that.x && this.y < that.y)) -1 else if (this.x == that.x && this.y == that.y) 0 else 1 } } |
最后几道练习不太容易,我觉得已经需要去看Java源码了。现在不在状态,放着吧。