21
class Class1<T>
{
    public virtual void Update(T entity)
    {
        Update(new List<T>() { entity }); //It's failed
    }

    public virtual void Update(IEnumerable<T> entities)
    {
    }

    public virtual void Update<TSub>(TSub entity) where TSub : T
    {
    }

    public virtual void Update<TSub>(IEnumerable<TSub> entities) where TSub : T
    {
    }
}

I have a piece of code. But it always failed.

If I replaced Update(new List<T>() { entity }) by Update((new List<T>() { entity }).AsEnumerable()), it will be ok.

It will be ok too when you delete the third method Update<TSub>(TSub entity) where TSub : T.

Can anybody tell me why?

Jailu Lee
  • 611
  • 1
  • 8
  • 13
  • 7
    you steped into the dark part of C# spec - Overload Resolution. Combined with generics, `params`, inheritance, polymorphism, generic contraints, `dynamic`, optional parameters - makes my hearbeat rate increase http://msdn.microsoft.com/en-us/library/aa691336(v=vs.71).aspx. +1 for good question btw – Ilya Ivanov Mar 22 '13 at 10:50

3 Answers3

20

OK, let's go through this carefully. We have

Update(new List<T>()); 

And three candidates -- note that we care only about the signatures of those candidates, so we'll strip away the return types and constraints, which are not part of the signature:

Update(IEnumerable<T> entities)
Update<U>(U entity) 
Update<V>(IEnumerable<V> entities) 

Our first task is to do type inference on those last two candidates. If inference fails then they are not applicable candidates.

Consider the second method

Update<U>(U entity) 

We have an argument of type List<T> and a formal parameter U. Therefore we infer that U is List<T>.

Consider the third method:

Update<V>(IEnumerable<V> entities)

We have an argument of type List<T> and a formal parameter of type IEnumerable<V>. List<T> implements IEnumerable<T> so we deduce that V is T.

OK, so our candidate list now consists of:

Update(IEnumerable<T> entities)
Update<List<T>>(List<T> entity) 
Update<T>(IEnumerable<T> entities) 

Are all of these candidates applicable? Yes. In each case List<T> is convertible to the formal parameter type. We cannot eliminate any of them yet.

Now that we have only applicable candidates we must determine which one is the unique best.

We can immediately eliminate the third one. The third one and the first one are identical in their formal parameter lists. The rule of C# is that when you have two methods that are identical in their formal parameter lists, and one of them got there "naturally" and one of them got there via type substitution, the substituted one loses.

We can also eliminate the first one. Clearly the exact match in the second one is better than the inexact match in the first one.

That leaves the second one as the last man standing. It wins the overload resolution fight. Then during final validation we discover that the constraint is violated: List<T> is not guaranteed to be a derived class of T.

Therefore overload resolution fails. Your arguments caused the best method chosen to be invalid.

If I call Update((new List<T>() { entity }).AsEnumerable()), it will be ok.

Correct. Go through it again. Three candidates:

Update(IEnumerable<T> entities)
Update<U>(U entity) 
Update<V>(IEnumerable<V> entities) 

We have an argument of type IEnumerable<T>, so we infer the second and third to be:

Update(IEnumerable<T> entities)
Update<IEnumerable<T>>(IEnumerable<T> entity) 
Update<T>(IEnumerable<T> entities) 

Now we have three applicable candidates with identical parameter lists. The ones that got there under construction are automatically worse than the natural ones, so we eliminate the second and third, leaving only the first. It wins, and it has no constraints to be violated.

It will be ok too when you delete the third method

Your statement is false; this will produce the same error as the first scenario. Taking away the third candidate does not cause the first candidate to suddenly start beating the second candidate.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • I'm still not quite understand why compiler needs to "guess" if we let constraints be part of method signature. The compiler checks the 3rd method (Update(...)), then infers the generic type to be List, then finds that List is not T, then eliminate the third one. – Mouhong Lin Mar 23 '13 at 05:29
  • @MouhongLin: See the article referenced in Andrew's answer. I have tried to explain a dozen times why constraints being part of the signature makes the results of overload resolution worse and hardly anyone agrees with me. – Eric Lippert Mar 23 '13 at 06:36
  • @eric-lippert, thanks for your kindly reply. It's much clear for me now. But I still have one more question. 'It will be ok too when you delete the third method'. I do not get the error when I delete the third method exactly. Can you please show me the reason? – Jailu Lee Mar 25 '13 at 06:19
  • @JailuLee: Post a small program that demonstrates the behaviour, so that we know we're talking about the same thing. – Eric Lippert Mar 25 '13 at 13:47
12

Constraints are not part of the signature, Eric Lippert has a great article about this topic.

Andrew
  • 1,102
  • 1
  • 8
  • 17
2

You are essentially asking why the compiler is not creating an implicit cast from List<T> to IEnumerable<T>. The reason is that the C# team made a deliberate design decision that cases of potential ambiguity must be resolved by the programmer, not the compiler. (Note that the VB.NET team made a different decision, to always attempt something sensible that is consistent with the perceived programmer intent.)

The advantages in a case such as this are that surprise is minimized - nothing unexpected can happen under the covers; the disadvantage is the occasional need for more verbose code.

Pieter Geerkens
  • 11,775
  • 2
  • 32
  • 52