我正在浏览effective scala slides,它在幻灯片10中提到永远不要在val
中使用trait
来抽象成员,而是使用def
。幻灯片没有详细提及为什么在val
中使用抽象trait
是一种反模式。如果有人可以解释在抽象方法的特性中使用val vs def的最佳实践,我将不胜感激
答案 0 :(得分:115)
def
可以由def
,val
,lazy val
或object
实现。所以这是定义成员最抽象的形式。由于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 }
您将无法定义F1
或F3
。
好的,混淆你并回答@ 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
的答案,因为它为实现提供了更多选项。
有些情况下您可能需要它们:
def
。需要了解的更重要的事情是何时可以安全地使用 val
覆盖某些内容以及使用不覆盖某些内容的 lazy 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
同样的规则适用于 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 val
或 lazy val
的 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
而绝不用于延迟初始化时的意思。
如果 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