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

Posted

技术标签:

【中文标题】何时在 Scala 特征中使用 val 或 def?【英文标题】:When to use val or def in Scala traits? 【发布时间】:2013-11-07 15:41:18 【问题描述】:

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

【问题讨论】:

【参考方案1】:

def 可以通过defvallazy valobject 中的任何一个来实现。所以它是定义成员的最抽象形式。由于特征通常是抽象接口,说你想要一个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 — 使用抽象 vals 可能会导致初始化问题:

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


object Fail extends Foo 
  val bar = 33


Fail.schoko  // zero!!

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

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

【讨论】:

关于棘手的初始化顺序和令人惊讶的空值的话? 是的......我什至不会去那里。的确,这些也是反对 val 的论据,但我认为基本动机应该只是隐藏实现。 这可能在最近的 Scala 版本中发生了变化(本评论为 2.11.4),但您可以使用 lazy val 覆盖 val。如果barval,您将无法创建F3 的断言是不正确的。也就是说,特征中的抽象成员应该始终是def's 如果将 val schoko = bar + bar 替换为 lazy val schoko = bar + bar,则 Foo/Fail 示例将按预期工作。这是对初始化顺序进行一些控制的一种方式。此外,在派生类中使用lazy val 而不是def 可以避免重新计算。 如果你把val bar: Int改成def bar: IntFail.schoko还是0。【参考方案2】:

我不喜欢在特征中使用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 虚拟机期望返回相同的类型。

【讨论】:

初始化顺序无关紧要,但我们会在运行时得到令人惊讶的 NPE,相对于反模式。 scala 具有隐藏命令性质的声明性语法。有时这种命令性违反直觉【参考方案3】:

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

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

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

要知道的更重要的事情是何时可以安全地使用 val 覆盖某些内容并拥有不覆盖某些内容的 lazy val


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

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

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

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

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


规则 2:切勿使用不会覆盖另一个 lazy valdeflazy val

据我所知,也没有充分的理由让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,那么打破这条规则可能是安全的,但即使这样也有腥味。

【讨论】:

我刚刚意识到规则 1 也适用于具有具体 vals 的类,这意味着如果一个类在其初始化的任何地方使用另一个 val,那么引用的 val 必须是最终的或扩展时有nulls 的风险。【参考方案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

【讨论】:

不相关。如果您使用 val 而不是 def(错误:重新分配给 val),也会出现错误,这是完全合乎逻辑的。 如果您使用var,则不会。关键是,如果它们是字段,则应将它们指定为字段。我只是认为将所有内容都设为def 是短视的。 @Dimitry,当然,使用var 让我们打破封装。但是使用def(或val)优于全局变量。我认为您正在寻找类似case class ConcreteEntity(override val id: Int) extends Entity 这样您就可以从def create(e: Entity) = ConcreteEntity(1) 创建它这比打破封装并允许任何类更改实体更安全。

以上是关于何时在 Scala 特征中使用 val 或 def?的主要内容,如果未能解决你的问题,请参考以下文章

Scala 中 val 可变与 var 不可变

Scala Slick模式特征中的“覆盖def *”是啥

scala def/val/lazy val区别以及call-by-name和call-by-value

scala--

Scala:定义案例类构造函数时“覆盖受保护的val”导致错误

对常量值使用 def 与 val 有何影响