C# generic method that receives List<T> doesn't call overloaded method for actual type of T (prefers generic one)

Viewed 236

I have this example C# code:

class Stuff { }  // empty class

void Main()
{
    var list = new List<Stuff> {
        new Stuff(),
        new Stuff()
    };
    Fun(list);
}

void Fun<T>(List<T> a)
{
    Debug.Log("called List<T> Fun");
    foreach (T t in a) {
        Fun(t);
    }
}

void Fun(Stuff a)
{
    Debug.Log("called Stuff Fun");
}

void Fun<T>(T a)
{
    Debug.Log("called T Fun");
}

Calling Main() ends up printing:

called List<T> Fun
called T Fun
called T Fun

I don't understand why the compiler is able to call Fun<T>(List<T> a) as expected, but then does not know to call Fun(Stuff a), which is more specific than Fun<T>(T a). Does it not know for sure at compile time that T is Stuff in this case? Printing typeof(T) inside Fun<T>(List<T> a) gives "Stuff" as expected, but that is not proof of anything...

Adding a Fun(List<Stuff> a) method works but is undesirable (lots of different possible types for Lists in the project, and the behaviour is supposed to be the same for all of them).

I have tried searching for this problem but couldn't phrase it in a way that I could find it. Sorry if someone asked this before (which is likely!).

1 Answers

The key is to understand that void Fun<T>(List<T> a) is compiled once, with overload resolution performed once. Not once per T, but a single time.

Consider the compiler situation when it's compiling this code:

void Fun<T>(List<T> a)
{
    Debug.Log("called List<T> fun");
    foreach (T t in a) {
        Fun(t);
    }
}

In particular, consider the overload resolution in the call to Fun(t).

The compiler knows nothing about T, which is the type of the argument in the Fun(t) call - it could be any non-pointer type. It has to perform overload resolution between these signatures:

void Fun<T>(List<T> a)
void Fun(Stuff a)
void Fun<T>(T a)

The only one of those methods which is applicable is the last - the T in the calling code is used as the type argument for the T in the method we're calling, and it's fine. The other two methods aren't applicable, because there's no conversion from T to List<TList> (for any TList), or from T to Stuff.

If you want the overload resolution to be performed at execution time instead, you could use dynamic typing:

foreach (dynamic d in a) {
    Fun(d);
}

I don't personally like doing that, but it might do what you want in this case. On the other hand, with nested lists it could get tricky - if you T is a List<int>, then would you expect it to call Fun<List<int>>(list) or Fun<int>(list)? I honestly can't remember the rules offhand to know which of those is "better" or whether it's ambiguous.

Related