5

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

使用冗余代码的工作示例

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实现中指定的特征?

4

3 回答 3

7

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

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

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

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

由于擦除,即使使用类型参数之类的技巧也不起作用。所以这不起作用:

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

(这可以在没有擦除的平台上工作,例如 .NET)

个人经验的一些建议

我认为虽然堆叠特征有时是一个不错的功能,但如果你有一个带有堆叠特征的大型继承层次结构,它可能是维护的噩梦。功能取决于混入特征的顺序,因此只需更改特征顺序即可破坏您的程序。

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

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

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

  1. 有一些工具可以强制执行某个命令。例如,如果 trait 中的方法未标记为 override,则不能将其混入已经实现该方法的类中。当然,如果您在 trait 中将方法标记为最终方法,则可以确保它始终处于“顶部”。无论如何,在特征中标记最终方法是一个非常好的主意。

  2. 如果您决定使用复杂的特征层次结构,则需要一种检查特征顺序的方法。这以 scala 反射的形式存在。使用反射查看此答案混合顺序

示例如何使用 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"

构建.sbt 并启动sbt console

于 2013-07-06T07:09:25.877 回答
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}"
}
于 2013-07-06T22:46:26.990 回答
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)
于 2017-09-20T12:29:29.753 回答