views:

853

answers:

2

Consider two extension methods:

public static T MyExtension<T>(this T o) where T:class
public static T MyExtension<T>(this T o) where T:struct

And a class:

class MyClass() { ... }

Now call the extension method on a instance of the above class:

var o = new MyClass(...);
o.MyExtension(); //compiler error here..
o.MyExtension<MyClass>(); //tried this as well - still compiler error..

The compiler says that calling the method is an ambiguous call when I call it on a class. I would have thought that it could determine which extension method to call, as MyClass is a class, not a struct?

+17  A: 

EDIT: I've now blogged about this in more detail.


My original (and I now believe incorrect) thought: generic constraints aren't taken into account during the overload resolution and type inference phases - they're only used to validate the result of the overload resolution.

EDIT: Okay, after a lot of going round on this, I think I'm there. Basically my first thought was almost correct.

Generic type constraints only act to remove methods from a candidate set in a very limited set of circumstances... in particular, only when the type of a parameter itself is generic; not just a type parameter, but a generic type which uses a generic type parameter. At that point, it's the constraints on the type parameters of the generic type which are validated, not the constraints on the type parameters of the generic method you're calling.

For example:

// Constraint won't be considered when building the candidate set
void Foo<T>(T value) where T : struct

// The constraint *we express* won't be considered when building the candidate
// set, but then constraint on Nullable<T> will
void Foo<T>(Nullable<T> value) where T : struct

So if you try to call Foo<object>(null) the above method won't be part of the candidate set, because Nullable<object> value fails to satisfy the constraints of Nullable<T>. If there are any other applicable methods, the call could still succeed.

Now in the case above, the constraints are exactly the same... but they needn't be. For example, consider:

class Factory<TItem> where TItem : new()

void Foo<T>(Factory<T> factory) where T : struct

If you try to call Foo<object>(null), the method will still be part of the candidate set - because when TItem is object, the constraint expressed in Factory<TItem> still holds, and that's what's checked when building up the candidate set. If this turns out to be the best method, it will then fail validation later, near the end of 7.6.5.1:

If the best method is a generic method, the type arguments (supplied or inferred) are checked against the constraints (§4.4.4) declared on the generic method. If any type argument does not satisfy the corresponding constraint(s) on the type parameter, a binding-time error occurs.

Eric's blog post contains more detail on this.

Jon Skeet
+1, and Paging Eric Lippert...
Richard
@Jon: I'm pretty sure that your original answer is correct, although I agree that it's difficult to see exactly where/how this is codified in the spec. Section 7.6.5.1 does seem ambiguous on this, but the idea that constraints aren't part of the signature is well-established (and confidently asserted by those who know these things, for example http://blogs.msdn.com/b/ericlippert/archive/2009/12/10/constraints-are-not-part-of-the-signature.aspx).
LukeH
My speculation on why your example doesn't compile... One of the rules in 7.5.3.2 says *"... if all parameters of Mp have a corresponding argument whereas default arguments need to be substituted for at least one optional parameter in Mq then Mp is better than Mq"*. According to that rule -- and assuming that constraints *aren't* taken into consideration -- then your second method turns out to be a better match for `Foo<int>()` even though it'll subsequently cause an error when the constraints are validated. (It's better because it requires no substitutions whereas the first method does.)
LukeH
Thanks Jon and LukeH - it looks as though type constraints aren't used when determining the method to use. I would create a answer myself for that, but I'd rather someone with more authority answer it :-)
Lee Atkinson
@Luke: But that should only come into play *after* all the candidate methods have been evaluated... and the second method shouldn't count as a candidate method, because it fails to satisfy the constraints. Note that even in the blog post you refer to, it explicitly states that the candidate set is determined before overload resolution. In fact, it *looks* to me like the blog post is inaccurate too (and I'm aware that this is heresy) - because although type inference succeeds, the result doesn't satisfy the constraints, so F shouldn't be part of the candidate set. Will add a comment to the blog
Jon Skeet
Actually, I don't think I'll add a comment to the blog, as there are over 5 pages worth of comments already...
Jon Skeet
Ooh, hang on... Eric actually addresses this in the blog post itself. More reading required. Although as it's about type *inference* which doesn't come into play in this Stack Overflow question, it may still be irrelevant...
Jon Skeet
Aha, it definitely *is* relevant, but hard to understand and explain!
Jon Skeet
Thanks for the comments - I've marked as the answer.
Lee Atkinson
+6  A: 

Eric Lippert explains better than I ever could, here.

I have come across this myself. My solution was

public void DoSomthing<T> (T theThing){
    if (typeof (T).IsValueType)
        DoSomthingWithStruct (theThing);
    else
        DoSomthingWithClass (theThing);  
}

// edit - seems I just lived with boxing

public void DoSomthingWithStruct (object theThing)
public void DoSomthingWithClass(object theThing)
Courtney de Lautour
Courtney, I couldn't get this to compile - within the DoSomething method, it says that it expects a value type to call DoSomthingWithStruct and a reference type to call DoSomthingWithClass.
Lee Atkinson
Ah, I'll have to check exactly what it was I did tomorrow
Courtney de Lautour