前言
本节将介绍Scala 中的特质。
特质是Scala 代码附中的基础单元。特质将方法和字段定义封装起来,然后通过将它们混入类的方式来实现复用。不同于类继承,类可以同时混入任意数量的特质。
环境:
Windows + Scala-2.12.8
1. 特质如何工作
特质和类的定义很像,如:
12.1.scala
trait Run {
def running() = {
println("I can running!")
}
}
一旦特征被定义好后,就可以用extends 或 with 关键字将它混入到类中。Scala 程序混入特质,而不是从特质继承,因为混入特质和其他很多语言的多重继承有重要的区别。混入Animal 的示例:
12.1.scala
class Dog extends Run {
override def toString = "Wang wang!"
}
运行示例:
特质同时也定义了一个类型。以下是Run 被用作类型的例子:
run 可以由任何混入了Run特质的类的对象初始化。
用with 混入特质:
12.1.scala
class Animal
class Dog extends Animal with Run {
override def toString = "Wang wang!"
}
混入多个特质:
class Animal
class HasLegs
class Dog extends Animal with Run with HasLegs {
override def toString = "Wang wang!"
}
Dog 类也可以重写running 方法。如:
12.1.scala
class Animal
class Dog extends Animal with Run {
override def toString = "Wang wang!"
override def running() = {
println("I can running and " + toString)
}
}
特质不能有任何“类”参数。
另一个类和特质的区别在于类中的super 调用是静态绑定的,而在特质中是动态绑定的。即如果类中写“super.toString”,你会确切的知道实际调用的是哪一个实现。在你定义特质的时候并没有被定义。具体是那个实现被调用,在每次该特质被混入到某个具体的类时,都会重新判定。这个奇特的行为是特质能实现可叠加修改的关键。
2. 瘦接口和富接口
特质的一个主要用途之一是自动给类添加基于已有方法的新方法。也就是说,特质可以丰富一个瘦接口,让它成为富接口。
瘦接口和富接口代表了我们在面向对象设计中经常面临的取舍,在接口实现者和使用者之间的权衡。要么实现者受罪,要么调用者受罪。
给特质添加具体方法让瘦接口和富接口之间的取舍变得严重倾向于富接口。不同于Java,给Scala特质添加具体方法是一次性的投入。你只需要再特质中实现这些方法一次,而不需要在每个混入该特质的类中重新实现一遍。
3. 示例:矩形对象
接下来举个例子来展示如何丰富接口。
图形库通常有许多不同的类来表示矩形。为了让这些矩形对象更加易于使用,我们的类库最好能提供一些坐标相关的查询,如width、height、left等。
首先不用特征的情况可能是这样,有一个Point 类 和 Rectangle:
12.2.scala
// 示例:矩形对象
class Point(val x: Int, val y: Int)
class Rectangle(val topLeft: Point, val bottomRight: Point) {
def left = topLeft.x
def right = bottomRight.x
def width = right - left
// and so on
}
可能还有另一个类是2D图形组件:
12.2.scala
// 2D图形
abstract class Component {
def topLeft: Point
def bottomRight: Point
def left = topLeft.x
def right = bottomRight.x
def width = right - left
// and so on
}
这两个类重复的代码很多。这些重复的代码可以用增值特质来消除。这个特质包含两个抽象方法:一个返回左上角的坐标,另一个返回右下角的坐标。然后,可以提供所有其他几何查询相关方法的具体实现。如:
12.2.scala
// trait
trait Rectangular {
def topLeft: Point
def bottomRight: Point
def left = topLeft.x
def right = bottomRight.x
def width = right - left
// and so on
}
然后,Rectangle就可以混入这个特质:
class Rectangle(val topLeft: Point,
val bottomRight: Point) extends Rectangular {
}
运行示例:
Ordered 特质
比较对象大小时另一个富接口会带来的便捷的领域。当你想要比较两个对象时,你可能需要在类中编写如 <、>、<= 等的方法。如:
class Rational(n: Int, d: Int) {
// ...
def < (that: Rational) =
this.numer * that.denom < that.numer * this.denom
def > (that: Rational) = that < this
// <= 和 >= 都是基于 < 实现的
}
不过,Scala 提供了专门的特质来解决。Ordered类的使用方式是将所有单独的比较方法替换成compare方法。Ordered特质为你定义了<、>、<=和>=,这些方法都是基于compare来实现的。比如,用Ordered 来对Rational(自己定义的一个有理数类)定义比较操作的代码:
完整代码见12.3.scala
// Ordered
class Rational(n: Int, d: Int) extends Ordered[Rational] {
// ...
// 如果this > that 返回负值, this == that 返回0
def compare(that: Rational) =
(this.number * that.denom) - (that.number * this.denom)
}
Ordered 要求在混入时传入一个类型参数,这个值就是你要比较的类型,如Rational,所以是Ordered[Rational]。
运行示例:
5. 作为可叠加修改的特质
现在你已经看过了特质的一个主要用途之一:将瘦接口转换成富接口。现在我们转向另一个主要用途:为类提供可叠加的修改。特质让你修改类的方法,而它们的实现方式允许你将这些修改叠加起来。
一个例子,对整个整数队列叠加修改。整个队列有两个操作:put,将整数放入队列;get,将它们取出来。队列符合先进先出原则。
可以定义特质来执行如下修改:
- Doubling:将放入队列的整数翻倍
- Incrementing:将放入队列的整数+1
- Filter:过滤放入队列的负数
这三个特质代表了修改,因为它们修改底下的队列类,而不是自己定义完整的队列类。这三个特质是可叠加的,可以从这三个特质中任意选择,将它们混入类,并得到一个带上你的选择的新的类。
如,这个给出一个抽象的IntQueue 类:
12.4.scala
// 5. 作为可叠加的特质
abstract class IntQueue {
def get(): Int
def put(x: Int)
}
用ArrayBuffer的IntQueue 的基本实现:
12.4.scala
import scala.collection.mutable.ArrayBuffer
class BasicIntQueue extends IntQueue {
private val buf = new ArrayBuffer[Int]
def get() = buf.remove(0)
def put(x: Int) = { buf += x }
}
运行示例:
然后,我们使用特质来修改这个行为。实现了对放入整数的翻倍:
12.4.scala
// Doubling
trait Doubling extends IntQueue {
abstract override def put(x: Int) = { super.put(x * 2) }
}
首先,Doubling声明了一个超类IntQueue,意味着这个特质只能被混入IntQueue 的子类。
另外,在该特质的一个声明为抽象的方法里做了一个super调用。这在普通类中是非法的。由于特质中的super调用时动态绑定的,只要在给出了该方法具体定义的特质或类之后混入,Doubling特质里的super调用就可以正常工作。
对于实现可叠加修改的特质,这样的安排通常是需要的。为了告诉编译器你是特意这样做的,必须将这样的方法标记为abstract override。它只能出现在特质中,它的含义是该特质必须混入某个拥有该方法具体定义的类中(如BasicIntQueue具体实现了put方法)。
运行示例:
因为Doubling 的混入,10 被翻倍。
MyQueue 并没有定义新的代码,它只是简单地给出一个类然后混入一个特质。这种情况下,可以用new 直接实例化,如:
为了搞清楚如何叠加修改,我们需要定义另外两个修改特质,Incrementing和Filtering:
12.4.scala
// +1
trait Incrementing extends IntQueue {
abstract override def put(x: Int) = { super.put(x + 1) }
}
// 过滤负数
trait Filtering extends IntQueue {
abstract override def put(x: Int) = {
if (x >= 0) super.put(x)
}
}
然后,你可以为特定的队列挑选想要的修改。如:
混入特质的顺序是重要的。一般来说,越靠右的特质越先起作用。如:
特质是一种从多个像类一样的结构继承的方式,不过它们跟许多其他语言中的多重继承有着重大的区别。其中一个区别尤为重要:对super 的解读。在多重继承中,super调用的方法在调用的地方就已经确定了。而特质中的super调用的方法取决于类和混入该类的特质的线性化。正是这个差别让可叠加修改变为可能。
(深入了解线性化可以查询更加详细的解释)