将单例对象编码为惰性val

时间:2014-07-21 16:38:00

标签: scala singleton semantics lazy-evaluation

上下文

我们正在研究Scala程序的静态验证程序(this Master's thesis中描述的早期工作),目前的重点在于验证涉及延迟评估的Scala功能。我们主要感兴趣的是功能的语义(行为),而不是其他(尽管很重要)方面,如可理解性或简洁性。

为简化起见,我们 - 暂时 - 忽略单个对象可能具有的特殊角色。例如,有些是伴随对象(可能与它们的懒惰性质正交),或者有些是包对象。

lazy vals和singleton对象的属性

懒惰的

假设懒惰的val

lazy val v = I

其中I初始化块,即确定延迟val值的代码。初始化块I在第一次取消引用延迟val v时执行。

单身对象

假设单个对象

object Foo {
  C1

  val v1 = I1
  var v2 = I2
  lazy val v3 = I3
  def m() {C2}
}

其中C1是构成对象Foo的构造函数的代码,其中I1I3再次是初始化块,其中C2是方法体m。首次使用对象Foo(取消引用或分配给变量/字段)时,将执行C1, I1I2I3仅在Foo.v3被取消引用时执行(因为v3是一个懒惰的val),并且只要C2被调用就会执行m

问题

考虑这个版本的Foo,其中单例对象由懒惰的val和匿名类编码:

// Should probably sit in a package object
lazy val Foo = new {
  C1

  val v1 = I1
  var v2 = I2
  lazy val v3 = I3
  def m() {C2}
}

有人能想出为什么单个对象Foo的编码作为一个懒惰的val会显示出与原始单例对象不同的行为的原因吗?也就是说,是否存在编码版本与原始代码具有不同语义的(角落)情况?

1 个答案:

答案 0 :(得分:0)

至少有一个主要的语义差异,我在下面解释过。我认为在多线程上下文中存在一些极端情况,因为对象是在其类的静态初始化器期间创建的,而静态初始化器与锁具有奇怪的交互。但我不是JVM多线程专家,所以我不知道细节。

无论如何,即使在单线程上下文中也会出现这种情况(至少在Manifest的实现中使用了 ,也许ClassTag s ,在当前的标准库中。)

考虑以下两个对象:

object A {
  val other = B
}
object B {
  val other = A
}

这是有效的Scala代码,访问A.otherB.other将正确返回另一个代码。这是因为A的“val”在调用超级构造函数后立即初始化。从本质上讲,你可以某种方式将上述对象转换为这个“低级”代码:

private var instanceA: AClass = null
def A(): AClass = {
  if (instanceA == null)
    new AClass
  instanceA
}
class AClass {
  def this() = {
    // reification of the constructor of A
    super()
    instanceA = this
    this.other = B()
  }
}

当然,B的翻译类似。您可以看到,在调用B()初始化A.other时,全局变量instanceA已经初始化。因此,当B创建并访问A()以初始化B.other时,def A()将立即返回A的正确实例。

lazy val没有这种早期初始化,因此将对象编码为简单lazy val s不会飞。访问任何一个对象将“死锁”,只有线程等待自己。