Scala中的特质详解

时间:2021-01-14 17:35:03

Scala中的特质与Java中的接口是比较类似的,但是Scala中的特质可以同时拥有抽象方法和具体方法,而类可以实现

个特质。下面我们详细讲解Scala中的特质这个强大的功能。

1. 把特质当作接口使用

我们定义一个trait,如下所示:

trait Logger {
def log(msg: String)
}

需要注意的是trait中未被实现的方法默认是抽象方法,因此不需要在方法前加abstract。

子类ConsoleLogger对Logger的实现,如下所示:

class ConsoleLogger extends Logger {
def log(msg: String): Unit = {
println(msg)
}
}

需要注意两点,一是实现时用的extends而不是implements;二是不需要在方法前写override关键字。

当我们需要的特质不止一个的时候,可以使用with关键字来添加其它的特质,如下所示:

class ConsoleLogger extends Logger with Cloneable with Serializable

我们使用Java类库中的Cloneable和Serializable接口作为特质,需要说明的是所有Java接口都可以作为Scala特质使用。


2. 带有具体实现的特质
带有具体方法的ConsoleLogger特质,如下所示:

trait Logger {
def log(msg: String): Unit = {
println(msg)
}
}
现在我们调用ConsoleLogger特质中的log(),如下所示:
class SavingsAccount extends Account with ConsoleLogger {  def withdraw (amount: Double): Unit = {    if (amount > balance)      log("Insufficient funds")    else      balance -= amount  }  ...}

本质来说,就是SavingsAccount类混入了ConsoleLogger特质的功能,但是这样也存在一个不好的地方,那就是当特

质发生改变时,所有混入了该特质的类都必须重新编译。


3. 带有特质的对象

我们定义Logged特质,如下所示:

trait Logged {
def log(msg: String): Unit = {
}
}

虽然Logged特质带了一个具体方法log(),但是该方法什么都不做。

我们在类SavingsAccount中使用Logged特质,如下所示:

class SavingAccount extends Account with Logged {
def withdraw (amount: Double): Unit = {
if (amount > balance)
log("Insufficient funds")
else
balance -= amount
}
...
}
貌似上面的代码毫无意义,但是我们可以在构造具体对象的时候混入一个更好的实现,如下所示:
trait ConsoleLogger extends Logged {  override def log(msg: String): Unit = {    println(msg)  }}
我们在构造对象acct的时候加入ConsoleLogger特质,如下所示:
val acct = new SavingsAccount with ConsoleLogger


4. 叠加在一起的特质

我们可以为类或对象添加多个互相调用的特质,并且调用从最后一个特质执行。这有什么用处呢?主要用于需要分阶

段加工处理某个值的场景。

假设我们想给所有的日志消息添加时间戳,如下所示:

val acct = new SavingsAccount with ConsoleLogger
trait TimestampLogger extends Logged {
override def log(msg: String): Unit = {
super.log(new java.util.Data() + " " + msg)
}
}
同样地,假设我们想要截断过于冗长的日志消息,如下所示:
trait ShortLogger extends Logged {  val maxLength = 15  override deg log (msg: String) {    super.log (      if (msg.length <= maxLength) msg else msg.substring(0, maxLength - 3) +        "...")  }}
特别注意的是,super.log并不像类那样拥有相同的含义,当然如果含义相同,那么这些特质毫无用处,因为这些特质

扩展的Logged特质中的log()什么也不做。实际上,super.log调用的是特质层级中的下一个特质,具体是哪一个,要

根据特质添加的顺序来决定。我们会通过举例子来加以说明顺序的作用,如下所示:

val acct1 = new SavingsAccount with ConsoleLogger with TimestampLogger with ShortLogger
val acct2 = new SavingsAccount with ConsoleLogger with ShortLogger with TimestampLogger

如果执行第一条语句,我们会得到消息Sun Feb 06 17:45:45 ICT 2011 Insufficient...这说明ShortLogger中的log()首先

被执行,然后它的super.log调用的是TimestampLogger。

如果执行第二条语句,我们会得到消息Sun Feb 06 1...这说明TimestampLogger中的log()首先被执行,然后它的

super.log调用的是ShortLogger,其结果在之后被截断。


5. 在特质中重写抽象方法

我们定义的Logger特质,如下所示:

trait Logger {
def log (msg: String): Unit = {
}
}
现在要在TimestampLogger特质中重写Logger特质中的log()抽象方法,如下所示:
trait TimestampLogger extends Logger {  abstract override def log(msg: String) {    super.log(new java.util.Date() + " " + msg)  }}
特别需要强调,Scala中trait的super.log与Java中继承的super.log并不一样,对trait而言,我们无法从源码判断

super.log会执行哪里的方法,确切的方法依赖于使用这些特质的对象或类给出的顺序。之所以加abstract关键字,是

因为Scala认为TimestampLogger仍然是抽象的,它需要混入一个具体的log()。


6. 当做富接口使用的特质

在Scala的trait中使用抽象方法和具体方法很常见,但是在Java中若要实现同样的功能,我们就需要声明一个接口和一

个扩展该接口的类。

让我们来丰富一下Logger特质,如下所示:

trait Logger {
def log (msg: String)
def into (msg: String): Unit = {
log("INFO: " + msg)
}
def warn (msg: String): Unit = {
log("WARN: " + msg)
}
def severe(msg: String): Unit = {
log("SEVERE: " + msg)
}
}
现在我们使用Logger特质的类就可以调用这些方法了,如下所示:
class SavingsAccount extends Account with Logger {  def withdraw (amount: Double): Unit = {    if (amount > balance)      severe("Insufficient funds")    else    ...  }  ...  override def log(msg: String): Unit = {    println(msg)  }}


7. 特质中的具体字段

特质中的字段可以是具体的,也可以是抽象的。如果你给出了初始值,那么字段就是具体的。如下所示:

trait ShortLogger extends Logged {
val maxlength = 15;
...
}

class Account {
var balance = 0.0
}

class SavingsAccount extends Account with ConsoleLogger with ShortLogger {
var interest = 0.0
def withdraw (amount: Double): Unit = {
if (amount > balance)
log("Insufficient funds")
else
...
}
}
由于在JVM中,一个类只能扩展一个超类,因此来自特质的字段不能以相同的方式继承。由于这个限制,maxLength

被直接加到SavingsAccount类中。因此,来自特质的字段被放置在子类中。最终SavingsAccount对象由所有超类的字

段(balance),以及任何子类中的字段(interest和maxLength)构成。


8. 特质中的抽象字段

特质中未被初始化的字段在具体的子类中必须被重写。

ShortLogger特质中的maxLength字段是抽象的,如下所示:

trait ShortLogger extends Logged {
val maxlength: Int;
override def log(msg: String): Unit = {
super.log(
if (msg.length <= maxlength) msg else msg.substring(0, maxlength - 3) + "..."
)
}
...
}
当我们在一个具体的类中使用该特质时,必须提供maxLength字段,如下所示:
class SavingsAccount extends Account with ConsoleLogger with ShortLogger {  val maxLength = 20  ...}

特别强调,maxLength字段前不需要加override关键字。这样一来,所有日志消息都将在第20个字符处被截断。肯定

有人会问,这样做究竟有什么好处呢?答案是这种提供特质参数值的方式在临时要构造某种对象时非常的方便。

现在我们定义一个实例,目的还是截断日志消息,如下所示:

val acct = new SavingsAccount with ConsoleLogger with ShortLogger {
val maxLength = 20
}


9. 特质构造顺序

构造器执行顺序,如下所示:

首先调用超类的构造器;

特质构造器在超类构造器之后,类构造器之前执行;

特质由左到右被构造;

每个特质当中,父特质先被构造;

如果多个特质共有一个父特质,而那个父特质已经被构造,则不会被再次构造;

所有特质构造完毕,子类被构造。

我们举一个例子来说明,如下所示:

class SavingsAccount extends Account with FileLogger with ShortLogger

构造器执行顺序,如下所示:

Account(超类);

Logger(第一个特质的父特质);

FileLogger(第一个特质);

ShortLogger(第二个特质);

SavingsAccount(类)。

特别强调的是,特质不能有构造器参数,每个特质都有一个无参数的构造器,由字段的初始化和其它特质体中的语句

构成。


10. 初始化特质中的字段

因为特质中的构造器都是无参数的,所以对于那些需要某种定制才有用的特质来说会是一个问题。

假定我们想要指定日志文件,但是又不能用构造参数,我们可能会想到利用FileLogger中的抽象字段来存放文件名,

但是考虑到构造器的执行顺序却是行不通的。解决这个问题的方案有2种,一个是提前定义,另一个是懒值。

(1)提前定义

val acct = new {
val filename = "myapp.log"
} with SavingsAccount with FileLogger

因为提前定义发生在常规的构造序列前,因此,在FileLogger被构造时,filename已经是初始化过的了,这样便不会

再抛出一个空指针异常。

如果我们是在类中做同样的事情,那么相应的语法如下所示:

class SavingsAccount extends {
val filename = "savings.log"
} with Account with FileLogger {
... // SavingsAccount implementation
}

(2)懒值

trait FileLogger extends Logger {
val filename: String
lazy val out = new PrintStream(filename)
def log (msg: String): Unit = {
out.println(msg)
}
}

val acct = new SavingsAccount with FileLogger {
val filename = "myapp.log"
}
由于被定义成lazy的out字段在初次使用时才会被初始化,而这个时候子类实例的filename字段已经设置好值了。需要

强调的是,这里的子类实例指的是new语句构造的其实是一个扩展自SavingsAccount(超类)并混入了FileLogger特

质的匿名类的实例。但是使用lazy也有一个不好的地方,那就是由于lazy在每次使用前都会检查是否已经初始化,它

们用起来并不是那么高效。


11. 扩展类的特质

特质不仅可以扩展另一个特质(比较常见),而且还可以扩展类(比较不常见),这个类将会自动成为所有混入该特

质的类的超类。

我们定义LoggedException特质扩展自Exception类,如下所示:

trait LoggedException extends Exception with Logged {
def log(): Unit = {
log(getMessage())
}
}

需要强调的是log()调用了从Exception超类继承下来的getMessage()。

现在让我们创建一个混入该特质的类,如下所示:

class UnhappyException extends LoggedException {
override def getMessage() = "arggh!"
}
特质的超类自动成为任何混入该特质的类的超类。但是如果我们的类已经扩展了另一个类该怎么办呢?答案是只要那

是特质的超类的一个子类即可,因为我们无法将2个类同时作为超类。


参考文献:

[1] 快速了解Scala技术栈:http://www.infoq.com/cn/articles/scala-technology/

[2] 《快学Scala》

[3] 快学Scala第十章特质课后习题解答:http://css.gxzj.com.cn/News.aspx?id=383960

相关文章