michael
michael

Reputation: 15282

Creating an extension method against a generic interface or as a generic constraint?

I'm not really sure if there is any real difference here in the two signatures:

public static class MyCustomExtensions
{
    public static bool IsFoo(this IComparable<T> value, T other)
        where T : IComparable<T>
    {
        // ...
    }

    public static bool IsFoo(this T value, T other)
        where T : IComparable<T>
    {
        // ...
    }
}

I think these will essentially operate almost identically, but I'm not quite sure... what am I overlooking here?

Upvotes: 1

Views: 5049

Answers (4)

Jeppe Stig Nielsen
Jeppe Stig Nielsen

Reputation: 61912

The difference is pretty obvious. Note that you have to define T either in the method (generic method) or in a containing class (generic class, not possible with extension methods). Below I call the two methods 1 and 2:

public static bool IsFoo1<T>(this IComparable<T> value, T other)
    where T : IComparable<T>
{
    return true;
}

public static bool IsFoo2<T>(this T value, T other)
    where T : IComparable<T>
{
    return true;
}

There are differences depending on whether T is a value type or a reference type. You can restrict to either by using constraint where T : struct, IComparable<T> or where T : class, IComparable<T>.

Generally with any type T: Some crazy type X might be declared IComparable<Y> where Y is distinct (and unrelated) to X.

With value types:

With IFoo1 the first parameter value will be boxed, whereas value in IFoo2 will not be boxed. Value types are sealed, and contravariance does not apply to value types, so this is the most important difference in this case.

With reference types:

With reference type T, boxing is not an issue. But note that IComparable<> is contravariant ("in") in its type argument. This is important if some non-sealed class implements IComparable<>. I used these two classes:

class C : IComparable<C>
{
    public int CompareTo(C other)
    {
        return 0;
    }
}
class D : C
{
}

With them, the following calls are possible, some of them because of inheritance and/or contravariance:

        // IsFoo1

        new C().IsFoo1<C>(new C());
        new C().IsFoo1<C>(new D());
        new D().IsFoo1<C>(new C());
        new D().IsFoo1<C>(new D());

        new C().IsFoo1<D>(new D());
        new D().IsFoo1<D>(new D());

        // IsFoo2

        new C().IsFoo2<C>(new C());
        new C().IsFoo2<C>(new D());
        new D().IsFoo2<C>(new C());
        new D().IsFoo2<C>(new D());

        //new C().IsFoo2<D>(new D()); // ILLEGAL
        new D().IsFoo2<D>(new D());

Of course, in many cases the generic argument <C> can be left out because it will be inferred, but I included it here for clarity.

Upvotes: 0

gabnaim
gabnaim

Reputation: 1105

They aren't identical. In the first one you are passing in IComparable<T> to the first but not the second, so your actual types would be <IComparable<IComparable<T>> and IComparable<T>.

EDITED based on Lee's feedback: these below look identical, but while both require value and other to implement IComparable, the second also requires that they are assignable to T.

public static bool IsFoo<T>(IComparable<T> value, IComparable<T> other)
{
    // ...
}

public static bool IsFoo<T>(T value, T other)
    where T : IComparable<T>
{
    // ...
}

Upvotes: 0

Lasse V. Karlsen
Lasse V. Karlsen

Reputation: 391276

Yes there is.

The first signature would match any type that can be compared to T, not just T values. So any type that implements IComparable<int> can be used by the first signature, not just int.

Example:

void Main()
{
    10.IsFoo(20).Dump();
    new Dummy().IsFoo(20).Dump();

    IComparable<int> x = 10;
    x.IsFoo(20).Dump();

    IComparable<int> y = new Dummy();
    y.IsFoo(20).Dump();
}

public class Dummy : IComparable<int>
{
    public int CompareTo(int other)
    {
        return 0;
    }
}

public static class Extensions
{
    public static bool IsFoo<T>(this IComparable<T> value, T other)
        where T : IComparable<T>
    {
        Debug.WriteLine("1");
        return false;
    }

    public static bool IsFoo<T>(this T value, T other)
        where T : IComparable<T>
    {
        Debug.WriteLine("2");
        return false;
    }
}

Will output:

2
False
1
False
1
False
1
False

I tested this with LINQPad.

Upvotes: 6

demoncodemonkey
demoncodemonkey

Reputation: 11957

If we rewrite it slightly to use IList instead of IComparable, wouldn't that be the same question?

In that case it is clear to see that IsFoo1 is completely different to IsFoo2.
Because IsFoo1 accepts first argument of essentially IList<IList<T>>
whereas IsFoo2 accepts first argument of just IList<T>

public static class MyCustomExtensions
{
    public static bool IsFoo1(IList<T> value, T other)
        where T : IList<T>
    {
        // ...
    }

    public static bool IsFoo2(T value, T other)
        where T : IList<T>
    {
        // ...
    }
}

So no they are not the same at all.

Upvotes: 0

Related Questions