【问题标题】:Encoding singleton objects as lazy vals将单例对象编码为惰性值
【发布时间】:2014-09-12 06:14:24
【问题描述】:

上下文

我们正在为 Scala 程序开发静态验证器(this Master's thesis 中描述的早期工作),当前的重点在于验证涉及惰性求值的 Scala 功能。我们主要对特征的语义(行为)感兴趣,而不是其他(尽管如此重要)方面,例如可理解性或简洁性。

为了简化事情,我们暂时忽略了单例对象可能具有的特殊角色。例如,有些是伴生对象(这可能与它们的惰性性质正交),或者有些是包对象。

惰性 val 和单例对象的属性

延迟验证

假设一个惰性值

lazy val v = I

Iinitialiser 块,即确定惰性值的值的代码。初始化程序块 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, I1I2被执行。 I3 仅在取消引用 Foo.v3 时执行(因为 v3 是一个惰性 val)并且每当调用 m 时都会执行 C2

问题

考虑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 会显示出与原始单例对象不同的行为的原因?也就是说,是否存在(角落)编码版本与原始代码具有不同语义的情况?

【问题讨论】:

  • 这是一个很好的问题——例如,参见 this answer 和 Miles Sabin 的评论,其中一个例子是 存在差异(不确定 应该是,不过)。
  • 感谢指点!不过,我对 Miles 评论的解释是,如果对象被编码为惰性 val,Poly1 的实现将无法编译,原因主要是由于名称解析。这与我想的不太一样,因为被编译器拒绝的代码显然表现出不同的行为。此外,如果问题真的“只是”由于名称解析,那么应该可以以不影响语言语义的方式解决它。但当然这只是我的猜测......

标签: scala singleton semantics lazy-evaluation


【解决方案1】:

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

无论如何,即使在单线程上下文中也会发生这种情况(并且 至少在Manifests 的实现中使用,并且可能在当前的ClassTags 中使用标准库)。

考虑以下两个对象:

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 vals 没有这种早期初始化,因此将对象编码为简单的lazy vals 将无法运行。访问任何一个对象都会“死锁”,唯一的线程在等待自己。

【讨论】:

  • 我不明白为什么超级构造函数在这里很重要。我运行了您的对象示例 (ideone.com/NSYJrv) 和相应的惰性 vals 编码 (ideone.com/uQR1ys),它们都表现出相同的行为:访问 A/B 会由于无限递归而导致 StackOverflowError。我观察到的唯一区别是编译器不能自动键入lazy val Arecursive value other needs type),这不是对象版本的问题,可能是因为对象不声明类型(new {...} 隐式声明)。顺便说一句,我想我们是在蒙彼利埃的 Scala'13 认识的。
  • 无论如何,感谢指向静态初始化程序的指针 - 我会尝试找出它们是否会有所作为。
  • 对,方法本地对象的行为不同。我的评论适用于顶级对象(或顶级对象内的对象,但不适用于方法内的对象):ideone.com/xfsQI8。超级构造函数并不重要,它只是实例被初始化的地方。
  • 感谢您的澄清!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-11-04
  • 1970-01-01
  • 2020-08-21
  • 2023-04-09
  • 2011-12-09
  • 2023-04-02
  • 2015-09-05
相关资源
最近更新 更多