【问题标题】:Scala - initialization order of valsScala - vals的初始化顺序
【发布时间】:2013-01-28 17:50:47
【问题描述】:

我有这段代码可以从文件中加载属性:

class Config {
  val properties: Properties = {
    val p = new Properties()
    p.load(Thread.currentThread().getContextClassLoader.getResourceAsStream("props"))
    p
  }

  val forumId = properties.get("forum_id")
}

这似乎工作正常。

我尝试将 properties 的初始化移动到另一个 val,loadedProperties,如下所示:

class Config {
  val properties: Properties = loadedProps
  val forumId = properties.get("forum_id")

  private val loadedProps = {
    val p = new Properties()
    p.load(Thread.currentThread().getContextClassLoader.getResourceAsStream("props"))
    p 
   }

}

但它不起作用! (propertiesproperties.get("forum_id") 中为空)。

为什么会这样?被properties 引用时,loadedProps 不被评估吗?

其次,这是初始化需要非平凡处理的变量的好方法吗?在 Java 中,我将它们声明为final 字段,并在构造函数中执行与初始化相关的操作。

在 Scala 中是否有这种场景的模式?

谢谢!

【问题讨论】:

    标签: scala constructor initialization immutability final


    【解决方案1】:

    Vals 按照声明的顺序进行初始化(准确地说,non-lazy vals 是),所以propertiesloadedProps 之前被初始化。或者换句话说,当properties被初始化时,loadedProps仍然是null。 这里最简单的解决方案是在properties之前定义loadedProps

    class Config {
      private val loadedProps = {
        val p = new Properties()
        p.load(Thread.currentThread().getContextClassLoader.getResourceAsStream("props"))
        p 
      }
      val properties: Properties = loadedProps
      val forumId = properties.get("forum_id")
    }
    

    您也可以使loadedProps 变得懒惰,这意味着它将在第一次访问时被初始化:

    class Config {
      val properties: Properties = loadedProps
      val forumId = properties.get("forum_id")
    
      private lazy val loadedProps = {
        val p = new Properties()
        p.load(Thread.currentThread().getContextClassLoader.getResourceAsStream("props"))
        p 
      }
    }
    

    使用惰性 val 的优点是您的代码对重构更加健壮,因为仅更改 val 的声明顺序不会破坏您的代码。

    同样在这种特殊情况下,您可以将loadedProps 转换为def(如@NIA 所建议的那样),因为它只使用一次。

    【讨论】:

    • 使用lazy对冲初始化订单问题可以很方便,但也有代价:1)accessor方法不再是简单的字段检索(会被HotSpot内联还是其他点播本机代码编译器?); 2)访问器包含同步码; 3) 每个实例都需要一个字段来保存对每个lazy val 的初始化状态进行编码的位。
    • 都是真的,但如果我们不得不谈论这些问题,第 (2) 点的成本是易失性访问的成本(实际同步仅发生在第一次访问时,实际初始化时)可以便宜很多。关于第 (1) 点,您必须意识到,即使使用普通的 val,您也将通过对 getter 的调用而不是直接访问底层文件(除非您的 val 是私有的 [this])。当然,该方法只是进行字段访问,并且可能在运行时更好地内联。更重要的是,在大多数情况下,这些(非常真实的)成本甚至都不是问题。
    • 显然,直接推论是这些成本在某些情况下可能成为一个问题,因此毫无疑问了解它们是件好事。
    • "你也可以让loadedProps变得懒惰,这意味着它会在第一次访问时被初始化"懒惰不是违反直觉的......(在这种情况下)?我真的希望 RHS 被评估并分配给 LHS,我已经看过 Scala 的常见问题解答,感觉就像一个笑话......它太复杂了,需要......
    【解决方案2】:

    我认为这里的loadedProps 可以简单地变成一个函数,只需将val 替换为def

    private def loadedProps = {
      // Tons of code
    }
    

    在这种情况下,您确定在调用它时会调用它。

    但不确定它是否是这种情况的模式

    【讨论】:

    • 另外,您可以使用 lazy 关键字使 loadedProps 变得惰性,并将它们移动到您的类声明之上。
    • 确实,使用 def 而不是 val,它的行为正确,但我不明白为什么。我希望在引用时对 loadedProps 进行评估。我想这可能与它位于右侧有关,但是规则是什么?如何避免事故发生?
    • @NIA 的回答是正确的。原因是vals 是从上到下初始化的,显然loadedProps' 的初始化器在forumId 的初始化器被评估时还没有被评估。
    • @teo 请参阅this entry
    【解决方案3】:

    只是添加了一点解释:

    您的properties 字段在此处初始化早于loadedProps 字段。 null 是初始化之前的字段值 - 这就是你得到它的原因。在def 的情况下,它只是一个方法调用,而不是访问某个字段,所以一切都很好(因为方法的代码可能会被调用多次——这里没有初始化)。见,http://docs.scala-lang.org/tutorials/FAQ/initialization-order.html。您可以使用deflazy val 来修复它

    为什么def 如此不同?那是因为def 可能会被调用多次,但val - 只有一次(所以它的第一个也是唯一一个调用实际上是fileld 的初始化)。

    lazy val只有在你调用它的时候才能初始化,所以它也有帮助。

    另一个更简单的例子:

    scala> class A {val a = b; val b = 5}
    <console>:7: warning: Reference to uninitialized value b
           class A {val a = b; val b = 5}
                            ^
    defined class A
    
    scala> (new A).a
    res2: Int = 0 //null
    

    更一般地说,理论上scala可以分析字段之间的依赖图(哪个字段需要其他字段)并从最终节点开始初始化。但实际上每个模块都是单独编译的,编译器甚至可能不知道这些依赖关系(甚至可能是 Java,它调用 Scala,它调用 Java),所以它只是进行顺序初始化。

    因此,它甚至无法检测到简单的循环:

    scala> class A {val a: Int = b; val b: Int = a}
    <console>:7: warning: Reference to uninitialized value b
           class A {val a: Int = b; val b: Int = a}
                                 ^
    defined class A
    
    scala> (new A).a
    res4: Int = 0
    
    scala> class A {lazy val a: Int = b; lazy val b: Int = a}
    defined class A
    
    scala> (new A).a
    java.lang.StackOverflowError
    

    实际上,这样的循环(在一个模块内)理论上可以在单独的构建中检测到,但它并没有太大帮助,因为它很明显。

    【讨论】:

    • 又一个 Scala "WTF" 时刻 ;-)
    猜你喜欢
    • 2015-05-19
    • 1970-01-01
    • 1970-01-01
    • 2010-09-17
    • 1970-01-01
    • 2011-10-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多