可堆叠特征中的继承和代码重用

时间:2013-07-06 06:27:10

标签: scala inheritance mixins traits

在这个简化的实验中,我希望能够快速构建一个具有可堆叠特征的类,该类可以报告用于构建它的特征。这让我想起了装饰器模式,但我更喜欢在编译时而不是在运行时实现它。

使用冗余代码的工作示例

class TraitTest {
  def report(d: Int) : Unit = {
    println(s"At depth $d, we've reached the end of our recursion")
  }
}

trait Moo  extends TraitTest {
  private def sound = "Moo"
  override def report(d: Int) : Unit = {
    println(s"At depth $d, I make the sound '$sound'")
    super.report(d+1)
  }
}
trait Quack extends TraitTest {
  private def sound = "Quack"
  override def report(d: Int) : Unit = {
    println(s"At depth $d, I make the sound '$sound'")
    super.report(d+1)
  }
}

执行(new TraitTest with Moo with Quack).report(0)然后会报告:

> At depth 0, I make the sound 'Quack'
  At depth 1, I make the sound 'Moo'
  At depth 2, we've reached the end of our recursion 

不幸的是,那里有很多冗余的代码让我的眼睛抽搐了。我试图清理它会让我:

没有冗余代码的非工作示例

class TraitTest {
  def report(d: Int) : Unit = {
    println(s"At depth $d, we've reached the end of our recursion")
  }
}

abstract trait Reporter extends TraitTest {
  def sound : String
  override def report(d: Int) : Unit = {
    println(s"At depth $d, I make the sound '${sound}'")
    super.report(d+1)
  }
}

trait Moo extends Reporter {
  override def sound = "Moo"
}
trait Quack extends Reporter{
  override def sound = "Quack"
}

当我们再次执行(new TraitTest with Moo with Quack).report(0)时,我们现在看到:

> At depth 0, I make the sound 'Quack'
  At depth 1, we've reached the end of our recursion

问题1:“Moo”的行在哪里?

我猜Scala一次只看到override def report(d: Int),因此只将它放在继承链中一次。我正在抓住稻草,但如果是这样的话,我该如何解决这个问题呢?

问题2:每个具体特性如何提供独特的sound

在解决第一个问题后,我会假设执行(new TraitTest with Moo with Quack).report(0)的结果看起来类似于以下内容,因为sound的继承可以起作​​用。

> At depth 0, I make the sound 'Quack'
  At depth 1, I make the sound 'Quack'
  At depth 2, we've reached the end of our recursion  

我们如何才能使每个特征使用其实现中指定的sound

3 个答案:

答案 0 :(得分:7)

特质最多可以遗传一次。它基本上只是一个扩展的java接口 scala编译器的非抽象方法。

构造具体类时,所有继承的特征都会线性化,因此您可以定义堆叠特征的顺序。如果继承两次特征,则只包含第一个特征。所以在

class C1 extends A with B 
class C2 extends C1 with X with B

线性化继承堆栈中B特征的位置将在A之后但在C1和X之前。第二个B mixin将被忽略。

即使使用类型参数这样的技巧也不会因擦除而起作用。所以这不起作用:

class X extends A with T[Int] with T[String]

(这适用于没有擦除的平台,如.NET)

个人经验的一些建议

我认为堆叠特征有时是一个很好的特性,如果你有一个具有堆叠特征的大型继承层次结构,它可能是一个维护噩梦。功能取决于特征混合的顺序,因此只需对特征顺序进行简单更改即可破坏程序。

此外,对不可变对象的类层次结构使用继承几乎需要使用显式自我类型类型参数,这会带来另一层复杂性。例如,请参阅scala集合中的xxxLike特征。

当它们不重叠时,特征当然非常有用且没有问题。但总的来说,规则支持组合优于继承对于scala和其他OO语言一样正确。 Scala为您提供了强大的特性继承工具,但它也为您提供了更强大的组合工具(值类,隐含,类型类模式......)

帮助管理大型特质层次结构

  1. 有一些工具可以强制执行某个订单。例如,如果特征中的方法未标记为覆盖,则不能将其混合到已实现该方法的类中。当然,如果您将某个方法标记为特征中的最终方法,则可以确保它始终处于“最顶层”。在任何情况下,在特征中标记方法最终是一个非常好的主意。

  2. 如果您决定使用复杂的特征层次结构,则需要一种方法来检查特征顺序。这以scala反射的形式存在。请参阅此答案mixin order using reflection

  3. 如何使用scala反射获取特征顺序的示例

    import scala.reflect.runtime.universe._
    class T extends TraitTest with Moo with Quack
    scala> typeOf[T].baseClasses
    res4: List[reflect.runtime.universe.Symbol] = 
      List(class T, trait Quack, trait Moo, class TraitTest, class Object, class Any)
    

    您需要在类路径中包含scala-reflect.jar,现在它是一个单独的依赖项。我刚刚使用了一个sbt项目,添加了

    libraryDependencies += "org.scala-lang" % "scala-reflect" % "2.10.2"
    

    到build.sbt并启动 sbt console

答案 1 :(得分:3)

以下是优选组合物的示例。放大逻辑被重构。

我发现我必须每年使用abstract override一次或两次,否则脑细胞会死亡。

在这个例子中,当你混入更多的噪音时,动物变得更吵。

它使用运行时反射,但当然你可以想象一个宏做类似的事情。 (你必须告诉它this是什么。)

真正的代码当然会执行更有趣的转换;例如,在鸭子噪音之后混入猪的声音听起来就像一只刚出蛋的鹅。

package sounds

trait Sound {
  def sound: String
}

trait Silent extends Sound {
  def sound: String = ""
}

// duck is always funnier
trait Duck extends Silent

object Amplifier {
  import reflect.runtime.currentMirror
  import reflect.runtime.universe._
  def apply[A <: Sound : TypeTag](x: Any): Int = {
    val im = currentMirror reflect x
    val tpe = im.symbol.typeSignature
    var i = -1
    for (s <- tpe.baseClasses) {
      if (s.asClass.toType =:= typeOf[A]) i = 0
      else if (s.asClass.toType <:< typeOf[Noise]) i += 1
    }
    i
  }
}

trait Noise
trait NoisyQuack extends Sound with Noise {
  abstract override def sound: String = super.sound + noise * amplification
  private val noise = "quack"
  private def amplification: Int = Amplifier[NoisyQuack](this)
}
trait NoisyGrunt extends Sound with Noise {
  abstract override def sound: String = super.sound + noise * amplification
  private val noise = "grunt"
  private def amplification: Int = Amplifier[NoisyGrunt](this)
}

object Test extends App {
  val griffin = new Duck with NoisyQuack with NoisyGrunt {
    override def toString = "Griffin"
  }
  Console println s"The $griffin goes ${griffin.sound}"
}

答案 2 :(得分:0)

我进行了一些修改,减少了代码重复,并提醒用户通过强制他将方法声明为super.report来调用abstract override

  trait TraitTest {
    def report(d: Int): Unit

    def reportSound(d: Int, sound: => String): Unit = {
      println(s"At depth $d, I make the sound '$sound'")
    }
  }

  trait TraitTestRoot extends TraitTest {
    def report(d: Int): Unit = {
      println(s"At depth $d, we've reached the end of our recursion")
    }
  }

  trait Moo extends TraitTest {
    private def sound = "Moo"

    abstract override def report(d: Int): Unit = {
      reportSound(d, sound)
      super.report(d + 1)
    }
  }

  trait Quack extends TraitTest {
    private def sound = "Quack"

    abstract override def report(d: Int): Unit = {
      reportSound(d, sound)
      super.report(d + 1)
    }
  }

  (new TraitTestRoot with Moo with Quack).report(0)