我正在查看effective scala幻灯片,它在第10页提到永远不要在trait
中使用val
来定义抽象成员,而是应该使用def
。幻灯片没有详细说明为什么在trait
中使用抽象的val
是一种反模式。如果有人能够解释在trait
中使用val
与def
来定义抽象方法的最佳实践,我将不胜感激。
我正在查看effective scala幻灯片,它在第10页提到永远不要在trait
中使用val
来定义抽象成员,而是应该使用def
。幻灯片没有详细说明为什么在trait
中使用抽象的val
是一种反模式。如果有人能够解释在trait
中使用val
与def
来定义抽象方法的最佳实践,我将不胜感激。
def
、val
、lazy val
或者 object
来实现,其中 def
是最抽象的形式。由于 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——使用抽象的val
可能会导致初始化问题:
trait Foo {
val bar: Int
val schoko = bar + bar
}
object Fail extends Foo {
val bar = 33
}
Fail.schoko // zero!!
这是一个很丑陋的问题,我个人认为在未来的Scala版本中应该通过在编译器中修复它来解决,但是,是的,目前这也是不应该使用抽象val
的原因之一。
编辑(2016年1月):您可以使用lazy val
实现覆盖抽象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 =
我同意其他答案关于避免使用抽象val
的原因是它为实现提供了更多选项。
有些情况下你可能需要它们:
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
,在我看来,这使得使用val
更加可接受。
如果您使用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
,而不是用于延迟初始化。
如果特质/类是final
,那么打破这个规则可能是安全的,但即使如此也有点可疑。
val
的类。这意味着,如果一个类在其初始化中使用了另一个val
,那么被引用的val
必须是final的,否则就会在扩展时冒着出现null
的风险。 - steinybot总是使用 def 似乎有点笨拙,因为像这样的东西不起作用:
trait Entity { def id:Int}
object Table {
def create(e:Entity) = {e.id = 1 }
}
error: value id_= is not a member of Entity
var
的话就不一样了。关键是,如果它们是字段,那么应该将它们指定为字段。我认为把所有东西都定义为 def
是短视的。 - Dimitryvar
会破坏封装性。但是,使用def
(或val
)优于全局变量。我认为你要找的是像case class ConcreteEntity(override val id: Int) extends Entity
这样的东西,这样你就可以从def create(e: Entity) = ConcreteEntity(1)
创建它。这比打破封装并允许任何类更改实体更安全。 - Jono
lazy val
覆盖val
。您声称如果bar
是val
,则无法创建F3
是不正确的。话虽如此,在特质中的抽象成员应始终是def
。 - mplisval bar: Int
改为def bar: Int
,Fail.schoko
仍然是零。 - Jasper-M