为什么对对象的 val 的 Scala 测试断言会抛出 NullPointerException?
Why a scala test assertion on an object's val throws NullPointerException?
在测试 Scala 代码时,我 运行 在断言对象的值时遇到了一个奇怪的 NPE。
这是重现问题的最少代码:
main/scala/Playground.scala:
object Playground extends App {
val greeting = "Hello Scala"
println(greeting)
}
test/scala/PlaygroundSpec.scala:
import org.scalatest.wordspec._
class PlaygroundSpec extends AnyWordSpec {
"The playground code" should {
"say hello" in {
assert(Playground.greeting.contains("Hello")) // Throws NPE because greeting is null. How???
}
}
}
示例程序 运行 很好并打印“hello Scala”,但测试在断言行上抛出 NullPointerException,因为 greeting
是 null
.
如果用字符串常量初始化,greeting
怎么会是null
呢?
Note: Adding lazy
to the val
declaration makes it work and the test passes.
您可以这样定义 Playground
:
object Playground /*extends App*/ {
val greeting = "Hello Scala"
println(greeting)
}
或者您可以这样定义 greeting
:
object Playground extends App {
lazy val greeting = "Hello Scala"
println(greeting)
}
对 Scala 2.x 这种奇怪行为的解释是 App
具有延迟加载功能。这在过去对许多其他人来说一直是个问题。 Scala 3 将改变这一点。
对于 Scala 2,最安全的选择是将对象引用移到 App
的主体之外,例如:
class Playground {
val greeting = "Hello Scala"
println(greeting)
}
object Playground extends App {
new Playground
}
在 Scala 2 App
extends DelayedInit
,所以编译器神奇地重写了初始化代码,这样字段的初始化就移到了 delayedInit
方法,例如,
object Playground extends App {
val greeting = "Hello Scala"
println(greeting)
}
变成类似
的东西
object Playground extends App {
private var greeting: String = null
def greeting(): String = greeting
def delayedInit(): Unit = {
greeting = "Hello Scala"
println(greeting())
}
def main(args: Array[String]) = {
// indirectly call delayedInit
...
}
}
现在我们可以看到
assert(Playground.greeting.contains("Hello"))
变成
assert(null.contains("Hello"))
as delayedInit
方法未被调用。为了证明这一点,请观察以下工作原理
Playground.main(Array.empty) // delayedInit gets indirectly called
assert(Playground.greeting.contains("Hello")) // ok
Adding lazy to the val declaration makes it work and the test passes.
之所以有效,是因为 lazy val greeting
有效地将字段转换为将其移出初始化代码的方法,因此它不会成为 delayedInit
的一部分。
显然这令人困惑,所以 Scala 3 Dropped: Delayedinit。
在测试 Scala 代码时,我 运行 在断言对象的值时遇到了一个奇怪的 NPE。
这是重现问题的最少代码:
main/scala/Playground.scala:
object Playground extends App {
val greeting = "Hello Scala"
println(greeting)
}
test/scala/PlaygroundSpec.scala:
import org.scalatest.wordspec._
class PlaygroundSpec extends AnyWordSpec {
"The playground code" should {
"say hello" in {
assert(Playground.greeting.contains("Hello")) // Throws NPE because greeting is null. How???
}
}
}
示例程序 运行 很好并打印“hello Scala”,但测试在断言行上抛出 NullPointerException,因为 greeting
是 null
.
如果用字符串常量初始化,greeting
怎么会是null
呢?
Note: Adding
lazy
to theval
declaration makes it work and the test passes.
您可以这样定义 Playground
:
object Playground /*extends App*/ {
val greeting = "Hello Scala"
println(greeting)
}
或者您可以这样定义 greeting
:
object Playground extends App {
lazy val greeting = "Hello Scala"
println(greeting)
}
对 Scala 2.x 这种奇怪行为的解释是 App
具有延迟加载功能。这在过去对许多其他人来说一直是个问题。 Scala 3 将改变这一点。
对于 Scala 2,最安全的选择是将对象引用移到 App
的主体之外,例如:
class Playground {
val greeting = "Hello Scala"
println(greeting)
}
object Playground extends App {
new Playground
}
在 Scala 2 App
extends DelayedInit
,所以编译器神奇地重写了初始化代码,这样字段的初始化就移到了 delayedInit
方法,例如,
object Playground extends App {
val greeting = "Hello Scala"
println(greeting)
}
变成类似
的东西object Playground extends App {
private var greeting: String = null
def greeting(): String = greeting
def delayedInit(): Unit = {
greeting = "Hello Scala"
println(greeting())
}
def main(args: Array[String]) = {
// indirectly call delayedInit
...
}
}
现在我们可以看到
assert(Playground.greeting.contains("Hello"))
变成
assert(null.contains("Hello"))
as delayedInit
方法未被调用。为了证明这一点,请观察以下工作原理
Playground.main(Array.empty) // delayedInit gets indirectly called
assert(Playground.greeting.contains("Hello")) // ok
Adding lazy to the val declaration makes it work and the test passes.
之所以有效,是因为 lazy val greeting
有效地将字段转换为将其移出初始化代码的方法,因此它不会成为 delayedInit
的一部分。
显然这令人困惑,所以 Scala 3 Dropped: Delayedinit。