Scala 的方法重载:与 varargs 和用或不带括号定义的 no-args 方法存在细微功能差异的原因?



在Scala(2.8.1)处理重载方法时遇到了一个奇怪的问题,其中第一个方法是无参数的方法,第二个方法接受可变数量的参数(0..N)。测试代码:

class Test {
  def method1 = println("method1 invoked with zero args")
  def method1(args: Any*) = println("method1 with args " + args)
  def method2() = println("method2 invoked with zero args")
  def method2(args: Any*) = println("method2 with args " + args)
}
object Test {
  def main(args: Array[String]) {
    val t = new Test
    t.method1
    t.method1()
    t.method1(1,2,3)
    println
    t.method2
    t.method2()
    t.method2(1,2,3)
  }
}

通过编译&运行它,输出如下:

method1 invoked with zero args
method1 with args WrappedArray()
method1 with args WrappedArray(1, 2, 3)
method2 invoked with zero args
method2 invoked with zero args
method2 with args WrappedArray(1, 2, 3)

如果运行带有圆括号和零参数的method1,我们将得到varargs方法,但在method2的情况下,将调用无参数方法。

这种奇怪的行为背后的解释或推理是什么?

好吧,那么这个问题可以重新表述为"为什么有人会认为这种行为是合乎逻辑的?":)

没有括号的方法不能用括号调用,所以在method1的情况下只有一个选择。

method2的情况下,调用t.method2()有两个重载选项可供选择,def method2()是一个更好的选择。

请参阅Scala语言规范第6.26.3节,了解如何选择最佳重载的精确描述:规则非常简单且符合逻辑。

这其实很符合逻辑。编译器模式匹配方法签名,并取最匹配的那个。

我还没有测试过,但是我很确定,你可以更进一步,像这样重载第二个方法:

def method2(arg: Any) = println("method2 invoked with a single arg " + arg)

所以如果你用一个参数调用它,编译器会选择这个,而不是varargs版本。

好吧,method1()不能是def method =的声明,因为如果没有参数列表传递参数列表,就不可能调用方法。

scala> def method1 = 0
method1: Int
scala> method1()
<console>:9: error: Int does not take parameters
              method1()
                     ^

请注意,这个错误是由于Scala试图调用method1的结果apply(),这是一个Int

相反,不能用形参列表调用变量方法。

scala> def vararg(x: Int*) = x.size
vararg: (x: Int*)Int
scala> vararg
<console>:9: error: missing arguments for method vararg;
follow this method with `_' if you want to treat it as a partially applied function
              vararg
              ^

因此,在第一种情况下,除了显示的行为之外,没有其他可能的行为。

在第二个示例中,第一个和最后一个用例是非二义性的。没有参数列表就不能调用变量(如图所示),没有参数传递参数就不能调用方法。可以调用带空参数列表的方法,不带参数,这样做主要是为了使Java api更"漂亮"——例如.toString

第二种情况——使用空形参列表调用方法——引入了一些歧义。然后Scala尝试确定一个是否比另一个更具体。

让我们在这里简单地绕一下。为什么要检查更具体的方法?假设你有这个:

object T {
  def f(x: AnyRef) = x.hashCode
  def f(x: String) = x.length
}

这种事情在Java api中相对常见。如果有人调用f("abc"),人们自然希望编译器调用第二个方法,而不是第一个方法,但是两个方法都是有效的。那么,如何消除它们的歧义呢?也许,由于String与传递的内容完全匹配,因此可能会使用它,对吗?那么,考虑一下这个:

object T {
  def f(x: AnyRef) = x.hashCode
  def f(x: java.util.Collection[_]) = x.size
}

我用f(new ArrayList[String])来命名它,现在呢?好吧,AnyRef是一个类,而Collection是一个接口,所以我们可以优先考虑接口而不是类?如果我有另一个方法期望List——那也是一个接口。

因此,为了解决这种歧义,Scala使用了最具体的概念。这实际上是一个很容易应用的概念。 首先,Scala验证一个是否与另一个一样特定。根据类型和表达式定义了四个简单的规则,但其要点是,如果b可以用a的形参调用,则方法a与方法b一样特定。

在这种情况下,method2()method2(args: Any*)一样具体,因为method2(args: Any*)可以用空参数列表调用。另一方面,method2(args: Any*)不像method2()那样具体,因为method2()不能用Any类型的参数调用。

接下来,Scala验证定义其中一个方法的类是否是定义另一个方法的类的子类。此规则不适用于这种情况。

最后,对于上面的两个条件,Scala给每个方法加1。所以method2()的权值是1,method2(args: Any*)的权值是0。如果其中一种方法的权重大于另一种,则认为该方法是最具体的

如我们所见,method2()是最具体的,所以选择它

相关内容

最新更新