7

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);
    }    
}
Lonimor
  • 678
  • 6
  • 10

3 Answers3

4

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.

Community
  • 1
  • 1
  • Nice links. I honestly didn't expect the compiler to be that clever. – Lonimor Dec 18 '15 at 17:13
  • 1
    @Lonimor You didn't think that it would be clever enough to compare the types of the expressions being used to the argument lists of the overloads of the method being called? Short of doing virtually nothing at compile time at all, and binding everything at runtime, there really isn't any other choice. – Servy Dec 18 '15 at 17:15
  • @Servy I understand the rules it uses for method overloading. I just didn't think that it knew the difference between null and Object dummy = null...but I suppose it makes more sense now thinking more about the clr types and references. – Lonimor Dec 18 '15 at 17:18
  • 1
    @Lonimor If `null` was an expression of compile time type `object` then you wouldn't be able to assign it to any variable that wasn't *also* of type `object` (without casting). It *needs* to be of some other type to allow you to write `Foo f = null;`. (In actuality of course, the null literal *has no type*; and it is implicitly convertible to *any* type. This isn't really the compiler being clever, this is just the fundamental nature about the definition of the `null` literal. – Servy Dec 18 '15 at 17:23
  • @Servy Thanks...that helps. – Lonimor Dec 18 '15 at 17:32
  • @Servy Thanks, added that point about the (non-) type of `null` to the answer. – 15ee8f99-57ff-4f92-890c-b56153 Dec 18 '15 at 17:35
3

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.

Matias Cicero
  • 25,439
  • 13
  • 82
  • 154
1

In your call to Test(name, v); your variable v as a Type of Object associated with it, even though it's value is null we still know "what kind of null" we have.

In your call to Test(name, null); that null has no type associated with it, so the compiler finds the nearest match and uses that overload. The nearest match is the object[] overload.

Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431