case对象初始化为null - 这怎么可能?

时间:2015-01-26 13:49:32

标签: scala heisenbug

我遇到了一个奇怪的错误。其中一个case对象(只有一个)获取null作为其值,显然会在生命的后期导致Null指针异常。但是:为什么?

我尝试了不同的东西(重命名对象,改变犹豫,甚至重写同一行两次,让其中一行编译好,而另一行给null)。我克隆了回购,以确保它不是一些有趣的传递效应。 Scala 2.11.4和2.11.5都有相同的原因。

代码相当曲折,切断它的上下文毫无意义。但在这里,如果它在别人身上敲响任何铃声。

sealed trait A {}

sealed trait B extends A {}

sealed trait C { val n: Int }
object C {
  val C1: C = new C { val n = 1 }
  val C2: C = new C { val n = 2 }
  val C3: C = new C { val n = 3 }
  val C4a: C = new C { val n = 4 }
  val C4b: C = new C { val n = 5 }
}

class D (depends: Seq[C], f: (Seq[Double]) => Double) {
  // ...methods removed
}

sealed trait E { val e: Int = 1 }

object D {
  import C._

  private def f( v: Seq[Double] ): Double = 0

  private def list( acc: Boolean ) = List(
    C1, C2, C3,
    if (acc) C4a else C4b
  )

  case object D1 extends D( list(true), f ) with E   // <-- this gets to be 'null'!!
  case object D2 extends D( list(false), f )

  val keys: Seq[D] = List(D1, D2)

  println(s"D1: $D1")
  println(s"D2: $D2")

  assert(D1 != null)    // <-- caught here
  assert(D2 != null)
}

我试图尽可能多地保留代码中的细节,以防它们以某种方式重要。

这可能是什么?

我将它标记为'heisenbug'但是当我编译东西时,它是一致的(即连续的测试运行总会产生相同的效果)。

编辑:我原来说这只发生在sbt test(而不是sbt run),但事实并非如此。两者都受到同样的影响。

编辑:似乎@dk14已将此归档为Scala issue 9115。谢谢!

2 个答案:

答案 0 :(得分:8)

我不知道到底发生了什么,但我怀疑是否涉及某种施工顺序。如果我粘贴代码并执行

,我可以100%重现问题
scala> D.D1
D1: null
D2: D2
java.lang.AssertionError: assertion failed
  at scala.Predef$.assert(Predef.scala:151)
  ... 47 elided

此外,如果我首先要求D2,它也会失败,因此它与E无关。

scala> D.D2
D1: D1
D2: null
java.lang.AssertionError: assertion failed
  at scala.Predef$.assert(Predef.scala:151)
  ... 47 elided

但如果我首先要求D,它会100%有效:

scala> D
D1: D1
D2: D2
res1: D.type = D$@50434135

scala> D.D1
res2: D.D1.type = D1

我成功将问题提炼为更短的代码示例:

class Bug(val f: Int) {}

object Bug {
  private def f(n: Int): Int = n

  case object Bug1 extends Bug(f(1))
  case object Bug2 extends Bug(f(2))

  val values: List[Bug] = List(Bug1, Bug2)

  println(s"1: $Bug1")
  println(s"2: $Bug2")
}

要求Bug.Bug1将失败。然而,如果我删除println行 - 一切正常。或者&#34;执行&#34; Bug首先。然而values没问题。

我想直接询问Bug.Bug1Bug.Bug2时,某些初始化锁会出错。

答案 1 :(得分:1)

仅当您在D.D1对象本身之前直接访问D.D2D时它才起作用,因为它未在此处初始化,因此case object值仍然是null。对我来说似乎是一个错误(应该在调用D之前初始化D.D1)。解决方法:在D之前的某处调用D.D1本身(以运行初始化)。请参阅https://stackoverflow.com/a/26072435/1809978 - 延迟初始化实际上是一项功能,但在您找到的情况下并非如此。简化示例:

object D {
  def aaa = 1 //that’s the reason
  class Z (depends: Any)
  case object D1 extends Z(aaa) // <-- this gets to be 'null'!!
  case object D2 extends Z(aaa)
  println(D1) 
  println(D2) 
}

结果:

defined object D
scala> D.D1
null
D2
res32: D.D1.type = D1

重新定义D后:

defined object D
scala> D.D2
D1
null
res34: D.D2.type = D2

因此scala forgots在运行封闭对象初始化之前初始化所请求的子对象(如果它引用了子对象内部的另一个对象成员的定义)。它在(定义aaa之后)初始化此子对象。我认为主要目的是在被请求的成员之前初始化所有被推荐的成员,但它仍然是一个错误,因为它改变了初始化顺序,这取决于代码中D的使用方式。