何时在 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
可以通过def
、val
、lazy val
或object
中的任何一个来实现。所以它是定义成员的最抽象形式。由于特征通常是抽象接口,说你想要一个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
您将无法定义 F1
或 F3
。
好的,为了让您感到困惑并回答 @om-nom-nom — 使用抽象 val
s 可能会导致初始化问题:
trait Foo
val bar: Int
val schoko = bar + bar
object Fail extends Foo
val bar = 33
Fail.schoko // zero!!
这是一个丑陋的问题,我个人认为应该通过在编译器中修复它在未来的 Scala 版本中消失,但是是的,目前这也是不应该使用抽象 val
s 的原因。
编辑(2016 年 1 月):您可以使用 lazy val
实现覆盖抽象的 val
声明,这样也可以防止初始化失败。
【讨论】:
关于棘手的初始化顺序和令人惊讶的空值的话? 是的......我什至不会去那里。的确,这些也是反对 val 的论据,但我认为基本动机应该只是隐藏实现。 这可能在最近的 Scala 版本中发生了变化(本评论为 2.11.4),但您可以使用lazy val
覆盖 val
。如果bar
是val
,您将无法创建F3
的断言是不正确的。也就是说,特征中的抽象成员应该始终是def
's
如果将 val schoko = bar + bar
替换为 lazy val schoko = bar + bar
,则 Foo/Fail 示例将按预期工作。这是对初始化顺序进行一些控制的一种方式。此外,在派生类中使用lazy val
而不是def
可以避免重新计算。
如果你把val bar: Int
改成def bar: Int
Fail.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】:我同意其他关于避免抽象 val
s 的答案,因为它为实现提供了更多选项。
在某些情况下您可能需要它们:
对于依赖于路径的类型(如 @0__ 所述)。 实现可能很昂贵,它用于具体的def
。
(还有其他人吗?如果有,请发表评论,我会添加)。
要知道的更重要的事情是何时可以安全地使用 val
覆盖某些内容并拥有不覆盖某些内容的 lazy val
。
规则 1:永远不要用非惰性 val
覆盖 val
或 def
,除非它是构造函数参数:
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
s,那么在我看来,这使得使用 val
s 更容易接受。
如果您使用 linter 强制执行 override
关键字并确保您的代码永远不会有任何 override val
定义,那么您很好。
您也许可以允许final override val
,但可能还有其他我没有想到的极端情况。
规则 2:切勿使用不会覆盖另一个 lazy val
或 def
的 lazy 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 也适用于具有具体val
s 的类,这意味着如果一个类在其初始化的任何地方使用另一个 val
,那么引用的 val
必须是最终的或扩展时有null
s 的风险。【参考方案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 def/val/lazy val区别以及call-by-name和call-by-value