views:

916

answers:

5

I don't understand why the compiler can't resolve the correct overload to use here. (code below) There is only one version of Add() that is appropriate- BigFoo is an IFoo, and does not implement IEnumerable where T is an IFoo. But it insists on reporting an ambiguity. Any ideas? I tried adding a second generic type parameter- Add where T : IFoo where U : IEnumerable. But then the overload is completely ignored even for legitimate use.

I know I can work around this with casting and specifying generic type parameters but at that point I've defeated the purpose of having an overload. You could question the overload, but the semantics feel correct to me- the behavior I'm implementing in my class is for both Add() to add the object wholesale as an individual entry in the collection. (the second Add() is not supposed to be an AddRange().)

namespace NS
{
  interface IFoo { }

  class BigFoo : IFoo, IEnumerable<int>
  {
    public IEnumerator<int> GetEnumerator()
    {
      throw new NotImplementedException();
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
      throw new NotImplementedException();
    }
  }

  class FooContainer
  {
    public void Add(IFoo item) { }
    public void Add<T>(IEnumerable<T> group) where T : IFoo { }
  }

  class DemoClass
  {
    void DemoMethod()
    {
      BigFoo bigFoo = new BigFoo();
      FooContainer fooContainer = new FooContainer();
      // error CS0121: The call is ambiguous between the following methods or properties: 
      // 'NS.FooContainer.Add(NS.IFoo)' and 
      // 'NS.FooContainer.Add<int>(System.Collections.Generic.IEnumerable<int>)'
      fooContainer.Add(bigFoo);
    }
  }
}
+2  A: 

Generic overload resolution doesn't take constraints into account, so it deems the Add<T> version to be applicable, inferring T=int.

Both methods are applicable, and neither is definitely better than the other, as there is no conversion between IEnumerable<int> and IFoo. While generic methods are deemed "less specific" than non-generic methods, this only becomes relevant when the parameter types are identical after type argument replacement, which they're not in this case.

Jon Skeet
Jeff Richter agrees "the C# compiler prefers a more explicit match over a generic match" Display("Jeff") would match Display(String) over Display<T>(T)
Gishu
The tie-breaking rules only apply if the formal parameter types are IDENTICAL. For example, if you have M(int x) and M<T>(T t) then the former is better than M<int>(int t).
Eric Lippert
Ah, thanks Eric. It's good to have the spec online and contributing ;) Will edit appropriately.
Jon Skeet
A: 

The compiler should be smart enough to recognize that BigFoo can't be cast to IEnumerable<IFoo>, but it isn't. It simply sees that it's an IEnumerable<T>, and feels that it's a potential overload candidate (even though the contstraint you defined enforces that T must be IFoo and int can't be cast to IFoo). While it's inconvenient, it's not that big of a deal. Just cast bigFoo to IFoo and the compiler will be happy:

fooContainer.Add((IFoo)bigFoo);

Alternately, you can make your generic overload of Add uglier:

public void Add<T, U>(U group)
    where T : IFoo
    where U : IEnumerable<T>
{
}

Either way, you have more typing, the second solution eliminates the need to cast calls to Add, but you will have to explicitly declare type on calls to the generic add (which ends up being more code:

fooContainer.Add<IFoo, IEnumerable<IFoo>>(enumerableFoo);
Michael Meadows
+1  A: 

In FooContainer, on the second "Add" you are constraining T to be of type IFoo. BigFoo implements the IFoo interface, therefore it kinda matches that Add definition (even though it doesn't really, because it doesn't implement IEnumable<IFoo>).

I'm not sure I understand completely what you want, but I suspect it is this:

public void Add<T>(T group) where T : IEnumerable<IFoo> { }

which would allow you to add any object T where T is an enumerable set of IFoo objects.

Is that what you wanted?

Regards, Richard

Richard J Foster
constraining T to IEnumerable<IFoo> instead of IFoo still doesn't help with overload resolution, though. Like Jon Skeet said, the compiler (unfortunately) won't take in to account constraints.
Michael Meadows
Ah... Cool. Learned something (else) new today. :-) Thanks for taking the time to comment - I really thought the compiler would consider constraints in the "where" clause, even if it didn't bother to consider them elsewhere in the method signature.
Richard J Foster
A: 

The problem here is that generic type constraints are completely ignored by the compiler (it only looks at parameter types). As far as the compiler is concerned, the IEnumerable<T> argument being passed could just as well be a IEnumerable<IFoo>.

For complete information on this subject, refer to section 25.6.4 Inference of type arguments of the C# Language Specification. Note that there is no mention of the utilisation of type constraints.

Noldorin
That's not the relevant place to look for the specification rules which are germane to this question. The relevant section in the 3.0 spec is 7.5.5.1, the bit that begins "final validation of the chosen best method is performed". As you can see from this section, the check for constraint violations is performed AFTER the check for uniqueness of the candidate set.
Eric Lippert
Ah right. Surely that section of the specification at least suggests the generic type constraints *are not* taking into account during the inference process, however?
Noldorin
Indeed, it is also the case that constraints are not used during inference.
Eric Lippert
A: 

Interesting.... Just tried your sample out. Generics continues to keep me on my toes.

//1 - First preference
public void Add(BigFoo item) { Console.WriteLine("static BigFoo type Add"); }
//2 - Second Preference
public void Add<T>(T item) { Console.WriteLine("Generic Add");  }
//3 - Third preferences
public void Add(IFoo item) { Console.WriteLine("static IFoo interface Add"); }
//4 - Compiles if 1-4 exist. Compile error (ambiguity) if only 3-4 exist. Compile error (cannot convert int to IFoo) if only 4 exists
public void Add<T>(IEnumerable<T> group) where T : IFoo { }
Gishu