99

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

4

4 回答 4

134

Adef可以通过 a def、 a val、 alazy val或 an来实现object。所以它是定义成员的最抽象形式。由于特征通常是抽象接口,说你想要一个val就是说实现应该如何做如果您要求 a val,实现类不能使用 a def

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


相比:

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 }

您将无法定义F1or F3


好的,为了让您感到困惑并回答@om-nom-nom - 使用 abstractval可能会导致初始化问题:

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

object Fail extends Foo {
  val bar = 33
}

Fail.schoko  // zero!!

这是一个丑陋的问题,在我个人看来,在未来的 Scala 版本中应该通过在编译器中修复它来解决它,但是是的,目前这也是不应该使用 abstractval的原因。

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

于 2013-10-28T18:24:46.057 回答
8

我不喜欢val在特征中使用,因为 val 声明的初始化顺序不明确且不直观。您可以向已经工作的层次结构添加一个特征,它会破坏以前工作的所有东西,请参阅我的主题:为什么在非最终类中使用普通 val

你应该记住所有关于使用这个 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 虚拟机期望返回相同的类型。

于 2013-10-28T20:23:41.473 回答
0

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

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

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

更重要的是要知道什么时候用 a 覆盖某些东西是安全的val,而让 alazy 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

相同的规则适用于 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用另一个覆盖 a ,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

鉴于我们已经将此规则应用于覆盖defs 那么这使得val在我看来使用 sa 更容易接受。

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

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


规则 2:永远不要使用lazy val不覆盖另一个lazy valor的 a def

据我所知,也没有充分的理由拥有一个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在绝对必要时使用而不是延迟初始化时的意思。

final如果特性/类别是但即使闻起来有腥味,打破这条规则可能是安全的。

于 2021-05-11T01:58:12.757 回答
-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
于 2015-06-15T18:34:22.140 回答