我正在浏览有效的 scala 幻灯片,它在幻灯片 10 上提到永远不要val
在trait
抽象成员中使用,def
而是使用。幻灯片没有详细提到为什么在 a 中使用抽象val
是trait
一种反模式。如果有人可以解释在抽象方法的特征中使用 val 与 def 的最佳实践,我将不胜感激
4 回答
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 }
您将无法定义F1
or 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
,这样也可以防止初始化失败。
我不喜欢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 虚拟机期望返回相同的类型。
我同意其他关于避免 abstractval
的答案,因为它为实现提供了更多选项。
在某些情况下您可能需要它们:
- 对于依赖于路径的类型(如@0__ 所述)。
- 实现可能很昂贵并且它用于具体的
def
. - (还有其他人吗?如果有,请发表评论,我会添加它们)。
更重要的是要知道什么时候用 a 覆盖某些东西是安全的val
,而让 alazy val
不覆盖某些东西是安全的。
规则 1 :除非它是构造函数参数,否则永远不要用非惰性覆盖val
或覆盖:def
val
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
鉴于我们已经将此规则应用于覆盖def
s 那么这使得val
在我看来使用 sa 更容易接受。
如果您使用 linter 来强制执行override
关键字并确保您的代码永远不会有任何override val
定义,那么您就很好。
您可能可以允许final override val
,但可能还有其他我没有想到的极端情况。
规则 2:永远不要使用lazy val
不覆盖另一个lazy val
or的 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
如果特性/类别是但即使闻起来有腥味,打破这条规则可能是安全的。
总是使用 def 似乎有点尴尬,因为这样的事情不起作用:
trait Entity { def id:Int}
object Table {
def create(e:Entity) = {e.id = 1 }
}
您将收到以下错误:
error: value id_= is not a member of Entity