何时在Scala特征中使用val或def?

时间:2013-10-28 18:10:17

标签: scala inheritance traits

我正在浏览effective scala slides,它在幻灯片10中提到永远不要在val中使用trait来抽象成员,而是使用def。幻灯片没有详细提及为什么在val中使用抽象trait是一种反模式。如果有人可以解释在抽象方法的特性中使用val vs def的最佳实践,我将不胜感激

4 个答案:

答案 0 :(得分:115)

def可以由defvallazy valobject实现。所以这是定义成员最抽象的形式。由于traits通常是抽象接口,所以你想要val实现应该如何。如果您要求val,则实施类不能使用def

只有在您需要稳定标识符时才需要val,例如对于路径依赖类型。这是你通常不需要的东西。


比较

trait Foo { def bar: Int }

object F1 extends Foo { def bar = util.Random.nextInt(33) } // ok

class F2(val bar: Int) extends Foo // ok

object F3 extends Foo {
  lazy val bar = { // ok
    Thread.sleep(5000)  // really heavy number crunching
    42
  }
}

如果你有

trait Foo { val bar: Int }

您将无法定义F1F3


好的,混淆你并回答@ om-nom-nom-using abstract val会导致初始化问题:

trait Foo { 
  val bar: Int 
  val schoko = bar + bar
}

object Fail extends Foo {
  val bar = 33
}

Fail.schoko  // zero!!

这是一个丑陋的问题,我个人认为它应该在未来的Scala版本中通过修复它在编译器中消失,但是,是的,目前这也是为什么不应该使用抽象val的原因。 / p>

编辑(2016年1月):您可以使用val实现覆盖抽象lazy val声明,这样也可以防止初始化失败。

答案 1 :(得分:7)

我不喜欢在特征中使用val因为val声明具有不清楚且不直观的初始化顺序。您可以向已经工作的层次结构添加特征,它会破坏之前有效的所有内容,请参阅我的主题:why using plain val in non-final classes

你应该记住使用这个val声明的所有事情,这最终会导致你的错误。


使用更复杂的示例进行更新

但有时你无法避免使用val。正如@ 0__有时提到你需要一个稳定的标识符而def不是一个。

我想举例说明他在说什么:

trait Holder {
  type Inner
  val init : Inner
}
class Access(val holder : Holder) {
  val access : holder.Inner =
    holder.init
}
trait Access2 {
  def holder : Holder
  def access : holder.Inner =
    holder.init
}

此代码产生错误:

 StableIdentifier.scala:14: error: stable identifier required, but Access2.this.holder found.
    def access : holder.Inner =

如果你花一点时间认为你会理解编译器有理由抱怨。在Access2.access情况下,它无法通过任何方式获得返回类型。 def holder意味着它可以广泛实施。它可以为每个调用返回不同的持有者,并且持有者将包含不同的Inner类型。但Java虚拟机需要返回相同的类型。

答案 2 :(得分:0)

我同意其他关于避免抽象 val 的答案,因为它为实现提供了更多选项。

有些情况下您可能需要它们:

  • 对于依赖于路径的类型(如@0__ 所述)。
  • 在实现可能很昂贵的地方,它用于具体的 def
  • (还有其他人吗?如果有,请发表评论,我会添加进来)。

需要了解的更重要的事情是何时可以安全地使用 val 覆盖某些内容以及使用不覆盖某些内容的 lazy val


规则 1:永远不要用非惰性 val 覆盖 defval,除非它是构造函数参数:

trait TraitWithVal {
  // It makes no difference if this is concrete or abstract.
  val a: String
  val b: String = a
}

class OverrideValWithVal extends TraitWithVal {
  // Bad: b will be null.
  override val a: String = "a"
}

class OverrideValWithLazyVal extends TraitWithVal {
  // Ok: b will be "a".
  override lazy val a: String = "a"
}

// Ok: b will be "a".
class OverrideValWithConstructorVal(override val a: String = "a") extends TraitWithVal

//class OverrideValWithDef extends TraitWithVal {
//  // Compilation error: method a needs to be a stable, immutable value.
//  override def a: String = "a"
//}

println((new OverrideValWithVal).b) // null
println((new OverrideValWithLazyVal).b) // a
println((new OverrideValWithConstructorVal).b) // a

同样的规则适用于 def

trait TraitWithDef {
  // It makes no difference if this is concrete or abstract.
  def a: String
  val b: String = a
}

class OverrideDefWithVal extends TraitWithDef {
  // Bad: b will be null.
  override val a: String = "a"
}

class OverrideDefWithLazyVal extends TraitWithDef {
  // Ok: b will be "a".
  override lazy val a: String = "a"
}

// Ok: b will be "a".
class OverrideDefWithConstructorVal(override val a: String = "a") extends TraitWithDef

class OverrideDefWithDef extends TraitWithDef {
  // Ok: b will be "a".
  override def a: String = "a"
}

println((new OverrideDefWithVal).b) // null
println((new OverrideDefWithLazyVal).b) // a
println((new OverrideDefWithConstructorVal).b) // a
println((new OverrideDefWithDef).b) // a

您可能想知道是否可以用另一个 val 覆盖 val,只要它在初始化期间不使用。至少有一个边缘情况打破了这一点:

trait TraitWithValAndLazyVal {
  val a: String = "A"
  def b: String = a
}

class OverrideLazyValWithVal extends TraitWithValAndLazyVal {
  // Bad: This on its own is ok but not if it is indirectly referenced during initialisation and overridden.
  override val a = "a"
  val c = b
}

class OverrideValWithVal extends OverrideLazyValWithVal {
  override val a = "a"
}

println((new OverrideValWithVal).a) // a
println((new OverrideValWithVal).b) // a
println((new OverrideValWithVal).c) // null

鉴于我们已经将此规则应用于覆盖 def,因此在我看来这使得使用 val 更容易接受。

如果您使用 linter 来强制使用 override 关键字并确保您的代码永远不会有任何 override val 定义,那么您就很好。

您也许可以允许 final override val,但可能还有其他我没有想到的边缘情况。


规则 2:切勿使用未覆盖另一个 lazy vallazy valdef

据我所知,也没有充分的理由让 lazy val 不是覆盖某些东西。我可以在需要的地方提出的所有示例,只是因为它违反了规则 1 并暴露了我之前描述的边缘情况。

例如:

trait NormalLookingTrait {
  def a: String
  val b: String = a
}

trait TraitWithAbstractVal extends NormalLookingTrait {
  val c: String
}

class OverrideValWithVal extends TraitWithAbstractVal {
  override def a: String = c
  override val c = "a"
}

println((new OverrideValWithVal).a) // a
println((new OverrideValWithVal).b) // null
println((new OverrideValWithVal).c) // a

所以我们将 b 设为 lazy val

trait SuspiciousLookingTrait2 {
  def a: String
  lazy val b: String = a
}

trait TraitWithAbstractVal2 extends SuspiciousLookingTrait2 {
  val c: String
}

class OverrideValWithVal2 extends TraitWithAbstractVal2 {
  override def a: String = c
  override val c = "a"
}

println((new OverrideValWithVal2).a) // a
println((new OverrideValWithVal2).b) // a
println((new OverrideValWithVal2).c) // a

看起来不错,除非我们更进一步:

trait SuspiciousLookingTrait2 {
  def a: String
  lazy val b: String = a
}

trait TraitWithAbstractVal2 extends SuspiciousLookingTrait2 {
  val c: String
}

class OverrideValWithVal2 extends TraitWithAbstractVal2 {
  override def a: String = c
  override val c = "a"
  val d = b
}

class OverrideValWithVal3 extends OverrideValWithVal2 {
  override val c = "a"
}

println((new OverrideValWithVal3).a) // a
println((new OverrideValWithVal3).b) // null
println((new OverrideValWithVal3).c) // a
println((new OverrideValWithVal3).d) // null

我现在明白人们所说的仅在绝对必要时使用 lazy 而绝不用于延迟初始化时的意思。

如果 trait / class 是 final,那么打破这个规则可能是安全的,但即使这样也有腥味。

答案 3 :(得分:-4)

总是使用def看起来有点尴尬,因为这样的东西不起作用:

trait Entity { def id:Int}

object Table { 
  def create(e:Entity) = {e.id = 1 }  
}

您将收到以下错误:

error: value id_= is not a member of Entity