Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.


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)

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();
    }
}