使用惰性参数初始化 class 时 Scala 编译器的奇怪行为

Strange behavior of Scala compiler when initializing a class with a lazy argument

怎么可能第一个是正确的 Scala 代码而第二个甚至不能编译?

编译的那个

object First {
  class ABC(body: => Unit) {
    val a = 1
    val b = 2
    println(body)
  }

  def main(args: Array[String]): Unit = {
    val x = new ABC {
      a + b
    }
  }
}

这个不能在 Scala 2.11 和 2.12 上编译

object Second {
  class ABC(body: => Int) {
    val a = 1
    val b = 2
    println(body)
  }

  def main(args: Array[String]): Unit = {
    val x = new ABC {
      a + b
    }
  }
}

一点也不奇怪。我们来看第一个例子:

您声明您的 class ABC 以接收 returns Unit 的按名称传递的参数并且您认为此片段:

   val x = new ABC {
      a + b
    }

正在传递 body 参数,它不是。真正发生的是:

val x = new ABC(()) { a + b }

如果你 运行 该代码你会看到 println(body) 打印 () 因为你没有为你的 body 参数,编译器允许它编译,因为正如 scaladoc 所述,只有 1 个 Unit:

类型的值

Unit is a subtype of scala.AnyVal. There is only one value of type Unit, (), and it is not represented by any object in the underlying runtime system. A method with return type Unit is analogous to a Java method which is declared void.

由于只有一个值,编译器允许您省略它,它会填补空白。单例对象不会发生这种情况,因为它们不会扩展 AnyValInt 的默认值是 0Unit 的默认值是 (),因为只有这个值可用,编译器接受它。

来自 documentation:

If ee has some value type and the expected type is Unit, ee is converted to the expected type by embedding it in the term { ee; () }.

单例对象不会扩展 AnyVal 因此它们不会得到相同的对待。

当您使用如下语法时:

new ABC {
 // Here comes code that gets executed after the constructor code. 
 // Code here can returns Unit by default because a constructor always 
 // returns the type it is constructing. 
}

您只是在向构造函数体中添加内容,而不是传递参数。

第二个示例无法编译,因为编译器无法推断出 body: => Int 的默认值,因此您必须显式传递它。

结论

构造函数括号内的代码与传递参数不同。在相同的情况下它可能看起来相同,但这是由于 "magic".

您不能将单个参数传递给花括号中的构造函数,因为这将被解析为 defining an anonymous class。如果要这样做,还需要将大括号括在普通大括号中,如下所示:

new ABC({
  a + b
})

至于为什么编译器会接受new ABC {a + b},解释有点复杂,出乎意料:

  1. new ABC {...} 等同于 new ABC() {...}
  2. new ABC() 可以解析为 new ABC(()) 因为自动元组,这是规范中没有提到的解析器的一个特性,见 SI-3583 Spec doesn't mention automatic tupling。相同的功能导致以下代码编译没有错误:

    def f(a: Unit) = {}
    f()
    
    def g(a: (Int, Int)) = {}
    g(0,1)
    

    请注意调用会产生警告(甚至您的原始示例也会):

    Adaptation of argument list by inserting () has been deprecated: this is unlikely to be what you want.

    警告是从 2.11 开始产生的,见问题SI-8035 Deprecate automatic () insertion