自身类型
特质可以要求混入它的类扩展自另一个类型,用自身类型(self type)的声明来定义特质:
this: 类型 =>
这样的特质只能被混入给定类型的子类当中。在如下示例中,LoggedException特质只能被混人扩展自 Expcetion的类:
trait Logged {
def log(msg: String)
}
trait LoggedException extends Logged{
this: Exception =>
def log(){ log(getMessage()) }
//可以调用getMessage ,因为this是一个Exception
}
如果尝试将该特质混入不符合自身类型所要求的类,就会报错:
val f = new JFrame with LoggedException
// 错误:JFrame不是LoggedException的自身类型Exception的子类型
如果想要提出多个类型要求,可以用复合类型:
this T with U with ... =>
注意:自身类型并不会自动继承。如果像如下这样定义:
trait ManagedException extends LoggedException{...}
就会得到错误提示,说ManagedException 并未提供Exception 。在这种情况下,你需要重复自身类型的声明:
trait ManagedException extends LoggedException {
this: Exception =>
...
}
依赖注入
在Scala中,可以通过特质和自身类型达到一个简单的依赖注入的效果。对日志功能而言,假定我们有如下特质:
trait Logger{ def log(msg: String) }
有该特质的两个实现:ConsoleLogger和FileLogger。
用户认证特质有一个对日志功能的依赖,用于记录认证失败:
trait Auth {
this: Logger =>
def login(id: String, password: String): Boolean
}
应用逻辑有赖于上述两个特质:
trait App {
this: Logger with Auth =>
...
}
然后,就可以像这样来组装自己的应用:
object MyApp extends App with FileLogger("test.log") with MockAuth("users.txt")
像这样使用特质的组合有些别扭。毕竟,一个应用程序并非是认证器和文件日志器的合体。它拥有这些组件,更自然的表述方式可能是通过实例变量来表示组件,而不是将组件黏合成一个大类型。蛋糕模式(cake pattern)给出了更好的设计,在这个模式当中,你对每个服务都提供一个组件特质,该特质包含:
- 任何所依赖的组件,以自身类型表述。
- 描述服务接口的特质。
- 一个抽象的val,该val将被初始化成服务的一个实例。
- 可以有选择性地包含服务接口的实现。
trait LoggerComponent{
trait Logger { ... }
val logger: Logger
class FileLogger(file : String) extends Logger{ ... }
...
}
trait AuthComponent{
this: LoggerCompoent => // 让我们可以访问日志
trait Auth{ ... }
val auth: Auth
class MockAuth (file: String) extends Auth{ ... }
...
}
注意我们对自身类型的使用,这段代码用来表明认证组件依赖日志器组件。
这样一来 ,组件配置就可以在一个集中的地方完成:
object AppComponents extends LoggerComponent with AuthComponent {
val logger = new FileLogger("test.log")
val auth = new MockAuth("users.txt")
}
不论是上述哪一种方式,都比在XML文件中组装组件要好,因为编译器可以帮我们校验模块间的依赖是被满足的。
抽象类型
类或特质可以定义一个在子类中被具体化的抽象类型(abstract type)例如:
trait Reader{
type Contents
def read(fileName: String): Contents
}
在这里,类型Contents抽象的。具体的子类需要指定这个类型:
class StringReader extends Reader {
type Contents = String
def read(fileName: String) = Source.fromFile(fileName, "UTF-8").mkString
}
class ImageReader extends Reader {
type Contents = Bufferedlmage
def read(fileName: String) = ImageIO.read(new File(fileName))
}
同样的效果也可以通过类型参数来实现:
trait Reader[C]{
def read(fileName: String): C
}
class StringReader extends Reader[String]{
def read(fileName: String) = Source.fromFile(fileName, "UTF- 8").mkString
}
class ImageReader extends Reader[Bufferedirnage]{
def read(fileName: String) = ImageIO.read(new File(fileName))
}
哪种方式更好呢?Scala的经验法则是:
- 如果类型是在类被实例化时给出的,则使用类型参数。比如构造HashMap[String, Int],你会想要在这个时候控制使用的类型。
- 如果类型是在子类中给出的,则使用抽象类型。我们的Reader实例就挺符合这个描述的。
在构建子类时给出类型参数并没有什么不好,但是当有多个类型依赖时,抽象类型用起来更方便一一你可以避免使用一长串类型参数。例如:
trait Reader {
type In
type Contents
def read(in: In): Contents
}
class ImageReader extends Reader {
type In = File
type Contents = BufferedImage
def read(file: In) = ImageIO.read(file)
}
用类型参数的话,ImageReader就需要扩展自Reader[File, BufferedImage]。这依然没问题,但你可以看得出来,在更复杂的情况下,这种方式的伸缩性并不那么好。
并且,抽象类型还能够描述类型间那些微妙的相互依赖。
抽象类型可以有类型界定,这就和类型参数一样。例如:
trait Listener {
class EventObject{...}
type Event <: EventObject
...
}
子类必须提供一个兼容的类型,比如:
trait ActionListener extends Listener{
class ActionEvent extends EventObject{...}
type Event = ActionEvent // OK ,这是一个子类型
...
}
注意:如果用类型参数,这个例子是无法完成的,因为绑定的是 个内部类。
家族多态
如何对那些跟着一起变化的类型家族建模,同时共用代码,并保持类型安全,是一个挑战。举例来说,我们可以看一下客户端Java的事件处理;这里有不同种类的事件(比如ActionEvent、ChangeEvent 等),而每种类型都有单独的监听器接口(ActionListener、ChangeListener等)。这就是“家族多态”的典型示例。
让我们来设计一个管理监听器的通用机制;我们首先用泛型类型,之后再切换到抽象类型。
在Java中,每个监听器接口有各自不同的方法名称对应事件的发生:actionPerformed、stateChanged、itemStateChanged等。 我们将把这些方法统一起来:
trait Listener[E]{
def occurred(e: E): Unit
}
而事件源需要一个监听器的集合和一个触发这些监听器的方法:
trait Source[E, L <: Listener[E]] {
private val listeners = new ArrayBuffer[L]
def add(l: L){ listeners += l }
def remove(l: L){ listeners -= l }
def fire(e: E){
for (l <- listeners ) l.occurred(e)
}
}
现在,我们来考虑按钮触发动作事件的情况 我们定义如下监听器类型:
trait ActionListener extends Listener[ActionEvent]
Button类可以混入Source特质:
class Button extends Source[ActionEvent, ActionListener]{
def click(){
fire(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "click"))
}
}
任务完成:Button类不需要重复那些监听器管理的代码,并且监听器是类型安全的,你没法给按钮加上ChangeListener。
ActionEvent类将事件源设置为this,但是事件源的类型为Object。我们可以用自身类型来让它也是类型安全的:
trait Event[S]{
var source: S = _
}
trait Listener[S, E <: Event[S]] {
def occurred(e: E): Unit
}
trait Source[S, E <: Event[S], L <: Listener[S, E]] {
this: S =>
private val listeners = new ArrayBuffer[L]
def add (l: L) { listeners += l )
def remove (l: L) { listeners -= l }
def fire (e: E) {
e.source = this // 这里需要自身类型
for (l <- steners) l.occurred(e)
}
}
注意自身类型this: S =>
,其用来将事件源设为this,否则,this只能是某种Source,而并不一定是Event[S]所要求的类型。
以下代码展示了如何定义一个按钮:
class ButtonEvent extends Event[Button]
trait ButtonListener extends Listener[Button , ButtonEvent]
class Button extends Source[Button, ButtonEvent, ButtonListener]{
def click(){ fire(new ButtonEvent) }
}
你可以看到类型参数扩张得很厉害,如果用抽象类型,代码会好看一些。
trait ListenerSupport {
type S <: Source
type E <: Event
type L <: Listener
trait Event {
var source: S = _
}
trait Listener {
def occurred (e: E): Unit
}
trait Source {
this : S =>
private val listeners = new ArrayBuffer[L]
def add(l: L) { steners += 1 }
def remove(l: L) { listeners -= 1 }
def fire (e: E) {
e.source = this
for (l <- steners) l.occurred(e)
}
}
}
但所有这些也有代价:你不能拥有顶级的类型声明,这就是所有代码都被包在ListenerSupport特质里的原因。
接下来,当你想要定义一个带有按钮事件和按钮监听器的按钮时,就可以将定义包含在一个扩展该特质的模块当中:
object ButtonModule extends ListenerSupport {
type SourceType = Button
type EventType = ButtonEvent
type ListenerType = ButtonListener
class ButtonEvent extends Event
trait ButtonListener extends Listener
class Button extends Source {
def click(){ fire(new ButtonEvent) }
}
}
如果要用这个按钮,引入这个模块即可:
object Main{
import ButtonModule._
def main(args: Array[String]){
val button = new Button
button.add(new ButtonListener{
override def occurred(e: ButtonEvent) { println(e) }
})
button.click()
}
}
高等类型
泛型类型List依赖于类型T并产出一个特定的类型 。举例来说,给定类型Int ,得到的是类型List[Int],因此,像List这样的泛型类型有时被称作类型构造器(type constructor)。在Scala中,可以更上一层,定义出依赖于依赖其他类型的类型的类型。
要搞清楚为什么这样做是有意义的,可以看看如下简化过的Iterable特质:
trait Iterable[E] {
def iterator(): Iterator[E]
def map[F](f: (E) => F): Iterable[F]
}
现在,考虑一个实现该特质的类:
class Buffer[E] extends Iterable[E]{
def iterator() : Iterator[E] = ...
def map[F] (f: (E) => F): Buffer[F] = ...
}
对于Buffer,我们预期map方法返回一个Buffer,而不仅仅是Iterable,这意味着我们不能在Iterable特质中实现这个map方法。一个解决方案是使用类型构造器来参数化Iterable,就像这样:
trait Iterable[E, C[_]] {
def iterator(): Iterator[E]
def build[F](): C[F]
def map[F] (f: (E) => F): C[F]
}
这样一来,Iterable就依赖一个类型构造器来生成结果,以C[ _ ]表示。这使得Iterable成为一个高等类型。
map方法返回的类型可以是,也可以不是与调用该map方法的原Iterable相同的类型,举例来说,如果你对Range执行map方法,结果通常不会是另一个区间,因此map必须构造出另一种类型,比如Buffer[F]。这样的一个Range类型声明如下:
class Range extends Iterable[Int, Buffer]
注意第二个参数是类型构造器Buffer。
要实现Iterable中的map方法,我们需要寻求更多的支持。Iterable需要能够产出一个包含了任何类型F的值的容器。我们定义一个Container类,某种你可以向它添加值的东西:
trait Container[E] {
def +=(e: E): Unit
}
而build方法被要求交出这样一个对象:
trait Iterable[E, C[X] <: Container[X]] {
def build[F](): C[F]
...
}
类型构造器C现在被限制为一个Container,因此我们知道可以往build方法返回的对象添加项目,我们不再对参数C使用通配符,因为我们需要表明C[X]是一个针对同样的X的容器。
有了这些以后,我们就可以在Iterable特质中实现map方法了:
def map[F](f: (E) => F): C[F] = {
val res = build[F]()
val iter = iterator()
while (iter.hasNext) res+= f(iter.next())
res
}
这样一来,可迭代(Iterable)的类就不再需要提供它们自己的map实现了。如下是Range类的定义:
class Range(val low: Int, val high: Int) extends Iterable[Int, Buffer] {
def iterator() =new Iterator[Int] {
private var i = low
def hasNext = i <= high
def next() = { i += 1; i - 1 }
}
def build[F] () = new Buffer[F]
}
注意Range是一个Iterable:你可以遍历其内容。但它并不是一个Container:不能对它添加值。而Buffer不同,它既是Iterable,也是Container。
class Buffer[E: ClassTag] extends Iterable[E, Buffer] with Container[E] {
private var capacity = 10
private var length = 0
// 为了构造出泛型的Array[E],类型E必须满足ClassTag上下文界定
private var elems = new Array[E](capacity)
def iterator() = new Iterator[E] {
private var i = 0
def hasNext = i < length
def next() = { i += 1; elems(i - 1) }
}
def build[F: ClassTag]() = new Buffer[F]
def +=(e: E) {
if (length == capacity) {
capacity = 2 * capacity
val nelems = new Array[E](capacity)
for (i <- 0 until length) nelems(i) = elems(i)
elems = nelems
}
elems(length) = e
length += 1
}
}
说明:要不带警告地使用高等类型,可添加如下的引入语句import scala.language.higherKinds
或使用编译器选项-language:higherKinds
。
上述示例展示了高等类型的典型用例。Iterator依赖Container,但Container不是一个普通的类型,它是制作类型的机制。
Scala集合类库中的Iterable特质并不带有显式的参数用于制作集合,而是使用隐式参数来带出一个对象用于构建目标集合。
参考:快学scala(第二版)