3

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!).

CanisLupus
  • 583
  • 5
  • 15
  • 2
    As a side-note, while it doesn't affect the answer, the question would be better if you followed .NET naming conventions (`Main`, `Fun` etc). It's slightly distracting to have the unconventional names. – Jon Skeet Sep 20 '18 at 10:47
  • Just try to comment your fun(T a) and you'll see your problem "cannot convert from 'T' to 'Stuff'" – Leonid Malyshev Sep 20 '18 at 10:58
  • @JonSkeet You are right and I can change the names, though that would cause your answer to be "out of sync". I will do that now. Can you edit yours? Thanks. [LeonidMalyshev]: I thought that was because there could be a miriad of types T could resolve to, and so it wouldn't be valid if it didn't have T to fall back to. I guess the reason is something else after all. ;) – CanisLupus Sep 20 '18 at 11:13
  • Yup, I've edited my answer now. – Jon Skeet Sep 20 '18 at 11:16
  • @JonSkeet: I thought the exact same thing per the naming conventions. It made the question very confusing to read in an almost dyslexic sort of way. – James Johnson Sep 20 '18 at 16:17

1 Answers1

11

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.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Hey Jon, thank you very much. All clear now! I expected the behaviour to be similar to C++ templates but I now realize that's not the case. `dynamic` solves the problem for me. The code in the project is only for debug mode. I do have nested lists but I will handle that. – CanisLupus Sep 20 '18 at 11:09
  • @CanisLupus: I second the dislike of using dynamic. I'm sure there are some cases where using dynamic is appropriate, but in my experience there is usually a better alternative available. It always seems like a lazy solution whenever I do see it being used, and I have come to generally consider it a code smell. – James Johnson Sep 20 '18 at 16:30
  • Hey James, I understand why, and either way it's the first time that I'm using `dynamic`. In this case it's really fine. The alternative would be to create a different method for each type of List we have, so using dynamic is probably the simpler and clearer I can do here. The actual code is for validation of a very large structure, which only runs in debug mode, so no performance issues either. – CanisLupus Sep 21 '18 at 09:02