Uncommenting the marked line below will cause a stackoverflow, because the overload resolution is favoring the second method. But inside the loop in the second method, the code path takes the first overload.
What's going on here?
private static void Main(string[] args) {
var items = new Object[] { null };
Test("test", items);
Console.ReadKey(true);
}
public static void Test(String name, Object val) {
Console.WriteLine(1);
}
public static void Test(String name, Object[] val) {
Console.WriteLine(2);
// Test(name, null); // uncommenting this line will cause a stackoverflow
foreach (var v in val) {
Test(name, v);
}
}
The second call works as you expect because val
in the second method is of type Object[]
, so in the foreach
, var v
is easily inferred to be of type Object
. There's no ambiguity there.
The second call is ambiguous: References of type Object
and Object[]
can both be null
, so the compiler has to guess which one you mean (more about that below). null
doesn't have any type of its own; if it did, you'd need an explicit cast to do pretty much anything with it, which would be unpleasant.
Overload resolution takes place at compile time, not runtime. The overload resolution in the loop isn't based on whether v
happens to be null
sometimes; that won't be known until runtime, long after the overload has been resolved by the compiler. It's based on the declared type of v
. The declared type of v
is inferred rather than explicitly declared, but the point is that it is known at compile time, when overloads are resolved.
In the other call where you explicitly pass null
, the compiler has to infer which overload you want using an algorithm (here's an answer in language which normal people can hope to understand) which in this case comes up with the wrong answer.
Of the two, it picks Object[]
because Object[]
can be cast to Object
, but the reverse is not true -- Object[]
is "more specific", or more specialized. It's farther from the root of the type hierarchy (or in plain English, in this case, one of the types is Object
and the other isn't).
Why is specialization the criterion? The assumption is that given two methods of the same name, the one with the more general argument type is intended to be the general case (you can cast anything to Object
), and the overload with a type farther towards the leaves of the type hierarchy will be intended to supersede the general-case method for certain specific cases: "Stick with this one for everything unless it's an array of Object
; I need to do something different for object arrays".
That's not the only imaginable criterion, but I can't think of any other half as good.
In this case, it comes out counterintuitive, because you think of null
as being as general as a thing can be: It's not even specifically Object
. It's... whatever.
The following would have worked, because here the compiler doesn't have to guess what you mean by null
:
public static void Test(String name, Object[] val) {
Console.WriteLine(2);
Object dummy = null;
Test(name, dummy);
foreach (var v in val) {
Test(name, v);
}
}
Short answer: Explicit nulls
make a mess of overload resolution, to the point where I sometimes wonder if it might not have been a mistake on the part of the language designers to let the compiler even try to figure them out (NB "I sometimes wonder if it might..." is not an expression of dogmatic certainty; the guys who designed the language are smarter than me).
The compiler's about as smart as it can be, which is "not very". It may have sporadic fits of outright malice, but this case is just good intentions gone wrong.
When overloading, if an ambiguity is encountered, the compiler will always try to execute the most specific method.
In this case, object[]
is more specific than object
.
null
can be any type, so it matches both method signatures. Since the compiler has to decide, it will opt for the Test(string name, Object[] val)
causing a StackOverflowException
.
Inside your foreach loop however, v
is inferred to be of type object
. Notice that now you have a typed variable.
As an object
, v
can either be an object
or an object[]
(or virtually any type), but the compiler doesn't know that, at least it won't know until runtime.
Overloading is resolved during compile time, so the only clue the compiler has is that v
is an object
, so it will chose to call Test(string name, Object value);
If you had the following line:
var val = new object[] { };
Test(name, val);
Then Test(string name, Object[] val)
would have been called, since the compiler knows that val
is an object[]
at compile time.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With