48

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?

Kobi
  • 135,331
  • 41
  • 252
  • 292
Lee Atkinson
  • 2,171
  • 3
  • 20
  • 32
  • Neat find! But what's your question? A workaround? – Eamon Nerbonne Oct 25 '10 at 11:24
  • 2
    Good question. I thought I had a simple answer to it, but it turns out I didn't. I hope you don't mind that my "answer" is more an exploration of what's going on than an answer in itself. – Jon Skeet Oct 25 '10 at 11:44
  • Thanks for your comments. Eamon - sorry, my question isn't that clear - it's really why the compiler cannot determine the best method to use. After reading the comments and questions, and the link provided by LukeH, it's because the compiler doesn't take into consideration the type constraints when determining the best method to use. – Lee Atkinson Oct 25 '10 at 12:43
  • Have edited my answer. I think I now understand what's going on, but it's pretty hard to explain... – Jon Skeet Oct 25 '10 at 13:40
  • Since constraints (and return types) are not a part of method signature, even merely declaring the two methods will give you compile error (for having same name for exact same definition). – nawfal Jul 16 '14 at 15:55

3 Answers3

36

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.

Glorfindel
  • 21,988
  • 13
  • 81
  • 109
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • @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 Oct 25 '10 at 12:41
  • 1
    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()` 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 Oct 25 '10 at 12:42
  • 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 Oct 25 '10 at 12:48
  • @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 Oct 25 '10 at 13:14
  • 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 Oct 25 '10 at 13:17
  • 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 Oct 25 '10 at 13:19
  • Aha, it definitely *is* relevant, but hard to understand and explain! – Jon Skeet Oct 25 '10 at 13:41
10

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)
Glorfindel
  • 21,988
  • 13
  • 81
  • 109
  • 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 Oct 25 '10 at 12:53
  • Ah, I'll have to check exactly what it was I did tomorrow –  Oct 25 '10 at 13:43
  • To avoid boxing, one should define a static class `SomethingDoer` with a field-backed read-only property of type `Action` called `DoSomething`; the class constructor should use Reflection to construct a delegate to call `DoSomethingWithStruct(T param) where T:struct`, `DoSomethingWithClass(T param) where T:class`, or `DoSomethingWithNullable(Nullable param)` and store it in that field. Reflection would only have to be used once for any given type parameter; after that, the delegate would invoke the appropriate method directly. – supercat Jul 10 '12 at 22:19
  • Why not make `DoSomthingWithStruct` make generic so that you can avoid boxing? – nawfal Jul 16 '14 at 15:40
5

I found this "interesting" strange way to do that in .NET 4.5 using default parameter values :) Maybe is more useful for educational\speculative purposes than for real use but I would like to show it :

/// <summary>Special magic class that can be used to differentiate generic extension methods.</summary>
public class MagicValueType<TBase>
    where TBase : struct
{
}

/// <summary>Special magic class that can be used to differentiate generic extension methods.</summary>
public class MagicRefType<TBase>
    where TBase : class
{
}

struct MyClass1
{
}

class MyClass2
{
}

// Extensions
public static class Extensions
{
    // Rainbows and pink unicorns happens here.
    public static T Test<T>(this T t, MagicRefType<T> x = null)
        where T : class
    {
        Console.Write("1:" + t.ToString() + " ");
        return t;
    }

    // More magic, other pink unicorns and rainbows.
    public static T Test<T>(this T t, MagicValueType<T> x = null)
        where T : struct
    {
        Console.Write("2:" + t.ToString() + " ");
        return t;
    }
}

class Program
{
    static void Main(string[] args)
    {

        MyClass1 t1 = new MyClass1();
        MyClass2 t2 = new MyClass2();

        MyClass1 t1result = t1.Test();
        Console.WriteLine(t1result.ToString());

        MyClass2 t2result = t2.Test();
        Console.WriteLine(t2result.ToString());

        Console.ReadLine();
    }
}
Salvatore Previti
  • 8,956
  • 31
  • 37
  • Why is the `[Serializable]` attribute needed? – Juan Jan 21 '15 at 18:42
  • I know this is old, but I just stumbled upon it and I wanted to add that you can do it much simpler. Any default argument in the signature that is different between the two will work. E.g. `public static T Test(this T value) where T : class => value;` vs `public static T Test(this T value, object _ = null) where T : struct => value;` – Mark Jul 10 '21 at 06:45