Scala -变量的初始化顺序

isr3a4wc  于 2023-03-12  发布在  Scala
关注(0)|答案(4)|浏览(135)

我有这样一段代码,从一个文件加载属性:

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

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

这看起来工作得很好。
我尝试过将properties的初始化移到另一个瓦尔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")中为空)。
为什么会这样呢?loadedProps不是在被properties引用时求值吗?
第二,这是初始化需要非平凡处理的变量的好方法吗?在Java中,我会声明它们为final字段,并在构造函数中执行与初始化相关的操作。
在Scala中,这种场景有模式吗?
谢谢大家!

mnemlml8

mnemlml81#

Vals按照声明的顺序初始化(准确地说,non-lazyvals是这样),所以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设置为lazy,这意味着它将在第一次访问时初始化:

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的声明顺序不会破坏代码。
同样在这个特定的示例中,您可以将loadedProps转换为def(如@NIA所建议),因为它只使用一次。

svgewumm

svgewumm2#

我认为这里loadedProps可以通过简单地将val替换为def来简单地转换为函数:

private def loadedProps = {
  // Tons of code
}

在这种情况下,您可以确保在调用它时它已被调用。
但不确定这是否是该病例的“模式”。

vatpfxk5

vatpfxk53#

只是添加了一点更多的解释:
你的properties字段在这里比loadedProps字段初始化的早。null是字段初始化前的值--这就是你得到它的原因。在def的情况下,它只是一个方法调用,而不是访问某个字段,所以一切都很好(因为方法的代码可能被调用多次-这里没有初始化)。请参见http://docs.scala-lang.org/tutorials/FAQ/initialization-order.html。您可以使用deflazy val来修复它
为什么def如此不同?这是因为def可以被调用几次,但是val只能被调用一次(所以它的第一次也是唯一一次调用实际上是初始化字段)。
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,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

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

clj7thdc

clj7thdc4#

正如其他人评论的那样,这是Scala中一个令人不安的问题,即使是在新的Scala 3上。
作为一种可能的解决方法,您可以使用deflazy val,如上所述,但它们甚至会带来其他后果和可能的性能影响。
此外,作为另一种选择,您可以激活一个名为-Xcheckinit的Scala编译器标志,以发出有关此类错误的警告...遗憾的是,它不能捕获所有初始化错误,并且额外添加了过多的插装代码,这严重影响了性能,因此只建议用于本地开发。
有关它的更多信息,请访问-〉https://docs.scala-lang.org/tutorials/FAQ/initialization-order.html

相关问题