Scala's method overloading: reason behind a subtle functionality difference with varargs and no-args method defined with or without parenthesis?
Encountered a weirdness in Scala (2.8.1) in handling an overloaded method where the first is a no-args one and the second takes a variable number of arguments (0..N). Test code:
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)
}
}
By compiling & running it, the output is:
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)
So if running method1
with parenthesis and zero arguments we get to the varargs method, but in method2
's case the no-args method is invoked.
What is the explanation or reasoning behind this weird behavior?
Ok, then the question could be reformulated as "why would someone think the behavior is logical?" :)
A method defined without parentheses can't be called with parentheses, so in method1
case there is only one option.
In method2
case, call t.method2()
has two overloaded options to choose from, and def method2()
is a better fit.
See Section 6.26.3 of The Scala Language Specification for a precise description of how the best overload is chosen: the rules are quite simple and logical.
It is actually very logical. The compiler pattern matches on method signatures and takes the one that matches the best.
I have not tested it, but I'm pretty sure, that you could go a step further and overload the second method like this:
def method2(arg: Any) = println("method2 invoked with a single arg " + arg)
So if you where to call it with one argument, the compiler would chose this one, instead of the varargs version.
Well, method1()
cannot be the def method =
declaration, since it is not possible to call a method without a parameter list passing a parameter list.
scala> def method1 = 0
method1: Int
scala> method1()
<console>:9: error: Int does not take parameters
method1()
^
Note that the error comes from Scala trying to call apply()
on the result of method1
, which is an Int
.
Conversely, one cannot call a vararg method with a parameter list.
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
^
Therefore, in the first case, there's no other possible behavior than what was shown.
In the second example, the first and last cases are non-ambiguous. One can't call a vararg without a parameter list (as shown), and one can't call a method without parameters passing a parameter. One can call a method with an empty parameter list without parameters, which was done mostly to make Java APIs more "pretty" -- for example, .toString
.
The second case -- calling the method with an empty parameter list -- introduces some ambiguity. Scala then tries to determine if one is more specific than the other.
Let's take a brief detour here. Why check for more specific methods? Suppose you have this:
object T {
def f(x: AnyRef) = x.hashCode
def f(x: String) = x.length
}
This kind of thing is relatively common on Java APIs. If someone called f("abc")
, one naturally expects the compiler to call the second method, not the first, but both are valid. So, how to disambiguate them? Perhaps, since String
is a perfect match for what is being passed, one might use that, right? Well, consider, then, this:
object T {
def f(x: AnyRef) = x.hashCode
def f(x: java.util.Collection[_]) = x.size
}
And I call it with f(new ArrayList[String])
So, what now? Well, AnyRef
is a class, while Collection
is an interface, so we might give precedence to interfaces over classes? And what if I had another method expecting List
-- that's also an interface.
So, to solve this ambiguity, Scala uses the most specific concept. That is actually a pretty easy concept to apply.
First, Scala verifies is one is as specific as the other. There are four simple rules defined in terms of types and expressions, but the gist of it is that method a is as specific as method b if b can be called with a's parameters.
In this case, method2()
is as specific as method2(args: Any*)
, because method2(args: Any*)
can be called with an empty parameter list. On the other hand, method2(args: Any*)
is not as specific as method2()
, because method2()
cannot be called with arguments of type Any
.
Next, Scala verifies if one the class that defines one of the methods is a subclass of the class that defines the other. This rule does not apply for this case.
Finally, Scala adds 1 to each method for each of the two criteria above it fits. So method2()
has weight 1, and method2(args: Any*)
has weight 0. If the weight of one is greater than the other, that method is considered to be the most specific.
As we saw, method2()
is the most specific one, so it is chosen.
精彩评论