7.1 Trait

Trait是指可以混入或融入一个类层次结构的行为。比如说,先对Friend建模,然后将其混入任何类:ManWomanDog等,而不用让它们都从一个公共的基类继承下来。

假定我们已经建模出Human,现在,想让它成为朋友。朋友是能够倾听你说话的人。所以,我们要给Human类增加一个listen方法,下面就是:

  1. class Human(val name: String) {
  2. def listen() = println("Your friend " + name + " is listening")
  3. }
  4. class Man(override val name: String) extends Human(name)
  5. class Woman(override val name: String) extends Human(name)

上面代码的一个不足之处在于,朋友这方面的特性不太突出,而且它被并到Human类里。另外,开发几周后,我们意识到我们忘记了人类最好的朋友——狗是伟大的朋友——当我们有太多的无法释怀时,它们会安静地听我们叙述。但是,怎样才能让狗成为我们的朋友呢?我们不能为此就让DogHuman继承下来。Java解决这个问题的方式是创建一个接口Friend,让HumanDog都实现这个接口。我们不得不在这两个类里提供不同的实现,不管实现是不是真的不同。

这就是Scala的trait介入的地方了。Trait像一个拥有部分实现的接口。trait里定义和初始化的valvar会在混入trait的类的内部得到实现。定义过而未初始化的valvar则认为是抽象的,需要由混入这些trait的类实现。下面将Friend这个概念重新实现为trait:

TraitsAndTypeConversions/Friend.scala

  1. trait Friend {
  2. val name: String
  3. def listen() = println("Your friend " + name + " is listening")
  4. }

这里,把Friend定义为trait。它有一个名叫nameval,被当作abstract对待。此外还有一个listen()方法。name实际的定义或实现由混入这个trait的类提供。下面来看一下混入上面这个trait的方式:

TraitsAndTypeConversions/Human.scala

  1. class Human(val name: String) extends Friend

TraitsAndTypeConversions/Man.scala

  1. class Man(override val name: String) extends Human(name)

TraitsAndTypeConversions/Woman.scala

  1. class Woman(override val name: String) extends Human(name)

Human类混入了Friend trait。如果类并不继承其他任何类的话,那么可以使用extends关键字混入trait。Human类及其派生类ManWoman简单的使用了trait提供的listen()方法的实现。如果需要的话,也可以改写这个实现,很快就会看到。

混入trait的数量可以是任意的。用关键字with就可以混入更多的trait。如果类已经继承了另一个类,就像下面这个例子里的Dog,还可以用关键字with混入第一个trait。除了混入trait之外,下面还在Dog里改写了listen()方法。

TraitsAndTypeConversions/Animal.scala

  1. class Animal

TraitsAndTypeConversions/Dog.scala

  1. class Dog(val name: String) extends Animal with Friend {
  2. //optionally override method here.
  3. override def listen = println(name + "'s listening quietly")
  4. }

一个类被混入trait之后,通过它的实例可以调用到trait的方法,也可以把它的引用当做trait的引用。

TraitsAndTypeConversions/UseFriend.scala

  1. val john = new Man("John")
  2. val sara = new Woman("Sara")
  3. val comet = new Dog("Comet")
  4. john.listen
  5. sara.listen
  6. comet.listen
  7. val mansBestFriend : Friend = comet
  8. mansBestFriend.listen
  9. def helpAsFriend(friend: Friend) = friend listen
  10. helpAsFriend(sara)
  11. helpAsFriend(comet)

上面代码的输出如下:

  1. Your friend John is listening
  2. Your friend Sara is listening
  3. Comet's listening quietly
  4. Comet's listening quietly
  5. Your friend Sara is listening
  6. Comet's listening quietly

trait看上去很像类,但是还有一些很大的差别。首先,它们需要混入类去实现那些已声明的而未初始化的(即抽象的)变量和值。其次,它们的构造器不能有任何参数。trait会编译成Java的接口,还有对应的实现类,里面包含了trait实现的方法。

多重继承通常会带来方法冲突的问题,trait并不会为这个问题所扰。通过延迟绑定混入类的方法,它们有效的回避了这一点。如此一来,在trait里调用super可能解析成另一个trait的方法,也可能会解析成混入类的方法,我们很快就会看到这一点。