scala学习笔记 - 特质

25 篇文章 0 订阅
15 篇文章 2 订阅

特质当接口使用

Scala的特质可以像Java的接口那样工作,如下:

trait Logger {
  def log(msg: String) // 定义一个抽象方法
}

无需使用abstract声明,特质中没有实现的方法默认就是抽象方法。子类可以实现,如下:

class ConsoleLogger extends Logger {
//  override def log(msg: String): Unit = println(msg) // 也可以
  def log(msg: String): Unit = println(msg) // 无须写override
}

可以用with关键字来添加多个特质,如下:

class ConsoleLogger extends Logger with Cloneable with Serializable

注意:在Sala中,Logger with Cloneable with Serializable首先是一个整体,然后再由类来扩展。

有具体实现的特质

在Scala中,特质中的方法并不需要一定是抽象的,如下:

trait Logger {
  def log(msg: String) // 定义一个抽象方法

  def logs(msg: String) {
    println(msg)
  }
}

带有特质的对象

在构造单个对象时,可以为它添加特质。先定义这样一个类:

class Demo

val demo = new Demo with ConsoleLogger
println(demo.log("========"))

叠加在一起的特质

可以为类或对象添加多个互相调用的特质,从最后一个开始。如下例子:

trait TimestampLogger extends ConsoleLogger { 
   override def log (msg: String) { 
      super.log(s"${java.time.Instant.now()} $msg" )
   }
}

对特质而言,super.log并不像类那样拥有相同的含义。实际上,super.log调用的是另一个特质的log方法,具体是哪一个特质取决于特质被添加的顺序。对于简单的混入序列而言,“从后往前”的规则会带给你正确的直觉判断。
对特质而言,你无法从源码判断super.someMethod会执行哪里的方法。确切的方法依赖于使用这些特质的对象或类给出的顺序。这使得super相比在传统的继承关系中要灵活得多。
如果你需要控制具体是哪一个特质的方法被调用,则可以在方括号中给出名称:super.[ConsoleLogger]. log( ... )。这里给出的类型必须是直接超类型;你不能使用继承层级中更远的特质或类。

特质中重写抽象方法

trait Logger {
  def log(msg: String) // 定义一个抽象方法
}
trait ConsoleLogger extends Logger {
  def log(msg: String): Unit = {
    super.log(msg)
  }
}

现在ConsoleLogger类编译通不过。
根据正常的继承规则,这个调用永远都是错的,Logger.log方法没有实现。但实际上,就像前面说到的,我们没法知道哪个log方法最终被调用,这取决于特质被混入的顺序。
Scala认为ConsoleLogger依旧是抽象的,它需要混入一个具体的log方法。因此你必须给方法打上abstract关键字以及override关键字,就像这样:

trait ConsoleLogger extends Logger {
  abstract override def log(msg: String): Unit = {
    super.log(msg)
  }
}

特质中的具体字段

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

trait ShortLogger extends Logger { 
	val maxLength = 15 // 具体的字段
	abstract override def log(msg: String) { 
		super.log( 
			if (msg.length <= maxLength) msg 
			else s"${ msg.substring(O, maxLength - 3) } ...")
	}
}

混入该特质的类将自动获得一个maxLength字段。一般而言,对于特质中的每一个具体字段 ,使用该特质的类都会获得一个字段与之对应。这些字段不是被继承的,它们只是简单地被加到了子类当中;这看上去像是一个很细微的差别,但这个区别很重要。
在JVM中,一个类只能扩展一个超类,因此来自特质的字段不能以相同的方式继承。由于这个限制, maxLength被直接加到了子类中,和子类字段一样,放在一起。可以把具体的特质字段当作针对使用该特质的类的“装配指令”,任何通过这种方式被混入的字段都自动成为该类自己的字段。
注意: 当你扩展某个类然后修改超类时,子类无须重新编译,因为虚拟机知道继承是怎么回事。不过当特质改变时,所有混入该特质的类都必须被重新编译。

特质的抽象字段

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

trait ShortLogger extends Logger { 
	val maxLength: Int // 抽象字段
	abstract override def log(msg: String) { 
		super.log( 
			if (msg.length <= maxLength) msg 
			else s"${ msg.substring(O, maxLength - 3) } ...")
			 // 在这个实现中用到了maxLength字段
	}
}

在一个具体的类中使用该特质时,必须提供maxLength字段。

特质构造顺序

和类一样,特质也可以有构造器,其由字段的初始化和其他特质体中的语句构成。例如:

trait FileLogger extends Logger { 
	val out= new PrintWriter("app.log")// 这是特质构造器的一部分
	out.println(s"# ${java.time.Instant.now()}") // 这同样是特质构造器的一部分
	def log(msg: String) {out.println(msg); out.flush()}
}

这些语句在任何混入该特质的对象于构造时都会被执行。
构造器以如下顺序执行:

  1. 首先调用超类的构造器;
  2. 特质构造器在超类构造器之后、类构造器之前执行;
  3. 特质由左至右被构造;
  4. 每个特质当中,父特质先被构造;
  5. 如果多个特质共有一个父特质,而那个父特质已经被构造,则该父特质不会被再次构造;
  6. 所有特质构造完毕,子类被构造。
    例如:
class Demo extends Demo1 with FileLogger with ShortLogger

构造器将按照如下顺序执行:

  1. Demo1(超类);
  2. Logger(第一个特质的父特质);
  3. FileLogger(第一个特质);
  4. ShortLogger(第二个特质,它的父特质Logger已经被构造);
  5. Demo(类)。
    说明: 构造器的顺序是类的线性化的反向。线性化是描述某个类型的所有超类型的一种技术规格,其按照以下规则定义:
    If C extends C1 with C2 with … with Cn , then lin© = C >> lin(Cn) >> … >> lin(C2) >> lin(C1)
    这里 >> 的意思是 “串接并去掉重复项,右侧胜出”。线性化给出了在特质中 super 被解析的顺序。

初始化特质中的字段

特质不能有构造器参数,每个特质都有一个无参数的构造器。缺少构造器参数是特质与类之间唯一的技术差别。除此之外,特质可以具备类的所有特性, 比如具体的和抽象的字段,以及超类。例如:

val demo = new Demo with FileLogger("myapp.log") // 错误:特质不能使用构造器参数

FileLogger可以有一个抽象的字段用来存放文件名。例如:

trait FileLogger extends Logger { 
	val filename: String
	val out= new PrintWriter("app.log")
	def log(msg: String) {out.println(msg); out.flush()}
}

使用该特质的类可以重写filename字段。不过很可惜,这里有一个陷阱,像这样直截了当的方案并不可行:

val Demo = new Demo with FileLogger{ 
	val filename = "myapp.log" // 这样行不通
}

问题出在构造顺序上FileLogger构造器先于子类构造器执行。这里的子类并不那么容易看得清楚;new语句构造的其实是一个扩展自Demo(超类)并混入了FileLogger特质的匿名类的实例。filename的初始化只发生在这个匿名子类中。实际上,它根本不会发生到子类之前,FileLogger的构造器就会抛出一个空指针异常。
解决办法之一:提前定义(early definition)。 以下是正确的版本:

val Demo = new { // new之后提前定义块
	val filename = "myapp.log"
} with Demo with FileLogger

提前定义发生在常规的构造序列之前。FileLogger被构造时, filename已经是初始化过的了。
解决办法二:是在FileLogger构造器中使用懒值,就像这样:

trait FileLogger extends Logger { 
	val filename: String
	lazy val out= new PrintWriter("app.log")
	def log(msg: String) {out.println(msg); out.flush()}
}

out字段将在初次被使用时才会初始化,而在那个时候,filename字段应该已经被设好值了;不过,由于懒值在每次使用前都会检查是否已经初始化,因此,它们用起来并非那么高效。

扩展类的特质

特质可以扩展另一特质,而由特质组成的继承层级也很常见,不那么常见的一种用法是,特质也可以扩展类,这个类将自动成为所有混入该特质的超类。
例如,LoggedException特质扩展自Exception类:

trait LoggedException extends Exception with ConsoleLogger{
	def log() { log(getMessage()) }
}

LoggedException有一个log方法用来记录异常的消息。注意log方法调用了从Exception超类继承下来的getMessage()方法。
特质的超类也自动地成为我们的类的超类,创建一个混入该特质的类:

class UnhappyException extends LoggedException{
	override def getMessage() = "xxx.log"
}

如果我们的类已经扩展了另一个类,只要是特质的超类的一个子类即可。 例如:

class UnhappyException extends IOException with LoggedException

这里的UnhappyException扩展自IOException,而这IOException已经是扩展自Exception了;在混人特质时,它的超类已经在那里了,因此无须额外再添加。不过,如果我们的类扩展自一个不相关的类,那么就不可能混入这个特质了。

自身类型

当特质扩展类时,编译器能够确保的一件事是,所有混入该特质的类都将这个类作为超类;Scala还有另一套机制可以保证这一点:自身类型(self type)。
当特质以如下代码开始定义时,

this: 类型 =>

它便只能被混人指定类型的子类。例如:

trait LoggedException extends ConsoleLogger{
	this: Exception =>
		def log(){log(getMessage())}
}

注意该特质并不扩展Exception类,而是有一个自身类型Exception。这意味着,它只能被混入Exception的子类。在特质的方法中,我们可以调用该自身类型的任何方法。
如果你想把这个特质混入一个不符合自身类型要求的类,就会报错,如下:

val f = new JFrame with LoggedException
// 错误: JFrame不是Exception的子类型,而Exception是LoggedException的自身类型

带有自身类型的特质和带有超类型的特质很相似。这两种情况都可确保混入该特质的类能够使用某个特定类型的特性。
在某些情况下自身类型这种写法比超类型版的特质更灵活,自身类型可以解决特质间的循环依赖,如果你有两个彼此需要的特质时,循环依赖就会产生。
自身类型也同样可以处理“结构类型(structural type)”,这种类型只给出类必须拥有的方法,而不是类的名称;以下是使用结构类型的LoggedException定义:

trait LoggedException extends ConsoleLogger{
	this: {def getMessage(): String}=>
		def log(){log(getMessage())}
}

这个特质可以被混入任何拥有getMessage方法的类。

参考:快学scala(第二版)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值