Search code examples
c#.netextension-methodsgenerics

Ambiguous call between two C# extension generic methods one where T:class and other where T:struct


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?


Solution

  • 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.