我们西北人很喜欢吃面,面馆里总有一口腾着热气的大锅。锅边站着一位壮汉,大力揉面,扯面。你去面馆点面的时候一般会被服务员问几个问题,加面?宽的细的?辣子香菜要不要?杂酱?西红柿?臊子?宽面劲道,细面入味,辣子爽口,杂酱,西红柿,臊子都不错,那就来个三合一。

服务员就是你和厨师之间的接口,接口就是隔离了调用者(你)和实现者(厨师)。

在Scala里没有接口(Interface),而是特质(Trait),特质比接口功能更加强大。Scala里不叫实现(implement) 了一个接口,而叫混入(mix-in)了一个Trait。叫做混入是有理由的。稍后我们就可以看到。

在Scala里特质里可以声明或者定义变量,也可以声明或者定义方法。声明的变量或方法不需要赋值或者实现。这些都交给具体的混入这个Trait的类去实现。看起来Scala里的Trait可以干所有Class可以干的事。但是它们之间还是有两点不同。

  1. Trait不能有构造函数。
  2. Trait里的super关键字指代和Class里不同。后边我们会讲到。

我们先介绍一下Scala里的Trait和Java里的Interface类似的用法:

规定契约,分离设计和实现。并且作为抽象类型实现多态。

如果你的类要直接混入一个特质,没有继承任何类。那么你需要使用 extends 关键字。如果你的类已经继承自一个父类,那么你需要用 with 关键字来混入一个特质。Scala里一个类只能继承自一个父类,但是可以混入多个特质。我们还是以面条为例。

package www.rethink.fun
trait Noodle {
  val brand = "www.rethink.fun"
  val price: Int
  val name: String
  def bill = s"Brand:$brand , Name:$name , Price:$price "
}
class NormalNoodle() extends Noodle{
  val name="NormalNoodle"
  val price =8
}
class BigNoodle() extends NormalNoodle{
  val name="BigNoodle"
  val price =10
}

上边我们定义了一个Noodle的特质,然后类NormalNoodle混入了Noodle这个特质。然后BigNoodle继承了NormalNoodle接下来我们看一下如何使用

val order = List[Noodle](new NormalNoodle(),new BigNoodle(),new NormalNoodle())
  order.foreach(noodle=>println(noodle.bill))

得到以下结果:

Brand:www.rethink.fun , Name:NormalNoodle , Price:8
Brand:www.rethink.fun , Name:BigNoodle , Price:10
Brand:www.rethink.fun , Name:NormalNoodle , Price:8

我们接下来要看一下Scala里Trait特有功能:

接口叠加

面条的选择有:是否加面,加香菜,加杂酱,加西红柿,加臊子,每种都可以选择是或者不是,一共有六种选项。因为每种选择不同导致我们做面的流程不同,如果我们要为每种类型实现一个子类,那么我们要实现2的6次方(64)个子类。
用Scala里的Trait,你有一个优雅的实现。首先我们去掉BigNoodle这个 class,将它变成一个Trait,叫做Big,然后再加上辣椒Trait,叫做Chilli,西红柿Trait,叫做Tomato。同时,我们把price从一个常量改为一个方法,我们来看一下实现:

package www.rethink.fun
trait Noodle {
  val brand = "www.rethink.fun"
  def price: Int
  val name: String
  def bill = s"Brand:$brand , Name:$name , Price:$price "
  def cook:String
}
class NormalNoodle() extends Noodle{
  val name="NormalNoodle"
  def price =8
  def cook=s"cooking $name"
}
trait Big extends Noodle{
   abstract override def price=super.price+2
  abstract override def cook = super.cook+" with more noodle"
}
trait Chilli extends Noodle{
  abstract override def cook = super.cook+" with chilli"
}
trait Tomato extends Noodle{
  abstract override def price=super.price+3
  abstract override def cook = super.cook+" with Tomato"
}

加面2元,加西红柿3元,加辣子免费,我们接下看一下怎么使用这几个Trait.

  val order = List[Noodle]( new NormalNoodle() with Big,                         //加面
                            new NormalNoodle() with Chilli with Tomato with Big, //加面加西红柿加辣子
                            new NormalNoodle() with Tomato)                      //加西红柿
  order.foreach(noodle=>println(noodle.cook+" Price:"+noodle.price))

我们得到的结果是:

cooking NormalNoodle with more noodle Price:10
cooking NormalNoodle with chilli with Tomato with more noodle Price:13
cooking NormalNoodle with Tomato Price:11

如果在Java里你该如何实现?如果你实现了多个接口,那么他们共同定义的cook方法和price方法就会冲突。在Scala里如果你在Trait的方法里调用super,这个super是Scala帮你动态寻找的,寻找的规则是从右向左,比如new NormalNoodle() with Chilli with Tomato with Big 里Big方法里的super指示的就是Tomato,Tomato的super就是Chilli,Chilli的super就是NormalNoodle。但是如果Big的cook方法里没有用super,那么只有Big的cook方法会被调用,其他Trait和NormalNoodle里定义的cook方法都不会被调用到。还有几点是你需要注意的:

  1. 这个super是动态寻找的,而类的继承关系在你定义的时候就确定了。
  2. 还有就是类的成员变量是无法使用这个特别的super的。
  3. 如果你的trait extends的是一个具体类,则不用在方法定义前加上abstract。比如trait Tomato extends NormalNoodle,则定义precie的时候前边不需要加abstract。
  4. Scala的trait是可以extends自一个类的,就比如上一条说的trait Tomato可以extends NormalNoodle

现在你应该觉得叫做混入Trait没那么拗口了吧。
“老板,给我的面条混入Tomato的特质!”
“休想,只有加了面条的大碗面才可以混入洋气的Tomato特质!”
老板想到一个绑定销售的策略,只有混入了Big的Noodle才能混入Tomato,这个需求有点让我这个饭量小的人难以接受,但是Scala里还是有这样的支持。我们来看:

trait Big extends Noodle{
  val size =2
   abstract override def price=super.price+2
  abstract override def cook = super.cook+" with more noodle"
}

我们修改了Big,给里边加了个常量size。表示它是普通分量的几倍, 我们接着来修改Tomato。

trait Tomato extends Noodle{
  bigTomato:Big=>
  abstract override def price=super.price+3
  abstract override def cook = super.cook+" with Tomato size "+bigTomato.size
}

注意我们加了一句bigTomato:Big=>, 这个没什么解释的,我们只要知道bigTomato就是你给this起的别名,你也可以在这里写成this。分号后边的Big就是表明Tomato特质依赖于Big特质。后边的=>符号是语法规定。在上边的例子中我们看到Tomato里已经可以调用Big里定义的size属性。
有了上边这句,下边的定义就会出错

 new NormalNoodle() with Tomato

Error:(31, 59) illegal inheritance;
 self-type www.rethink.fun.NormalNoodle with www.rethink.fun.Tomato does not conform to www.rethink.fun.Tomato's selftype www.rethink.fun.Tomato with www.rethink.fun.Big
                                  new NormalNoodle() with Tomato )

加上这个限制,面馆老板在他捆绑销售的道路上一骑绝尘了…

1 对 “Rethink Scala 之七:Trait”的想法;

发表评论

电子邮件地址不会被公开。 必填项已用*标注

%d 博主赞过: