Friday, July 20, 2007

Constrained generic extension methods

When I first saw extension methods, a new feature in C# 3.0, I was a bit skeptical.  It seemed like yet another language feature shoehorned in to support LINQ.  After going a few rounds with the technology, it actually looks quite promising.  For a full explanation of extension methods and what scenarios they can enable, check out Scott Guthrie's post on the subject.

Making the extension method generic

First, let's examine the original example from Scott's post, which adds an "In" method to test whether an item is in a collection:

public static bool In(this object o, IEnumerable items)
{
    foreach (object item in items)
    {
        if (item.Equals(o))
            return true;
    }

    return false;
}

The "In" method will be added to all objects, including primitive value types such as "int" and "double".  Here's a sample snippet using "int":

int[] values = {5, 6, 10};

bool isInArray = 7.In(values);

The value "isInArray" evaluates to "false", as the number 7 isn't in the array.  So what's wrong with this code?  The problem is that the "In" method uses the type "object" to extend types, but that will cause boxing when the number 7 is boxed to an object for the extension method call.  Generics can prevent boxing from occurring, so let's change the extension method to be generic:

public static bool In<T>(this T o, IEnumerable<T> items)
{
    foreach (T item in items)
    {
        if (item.Equals(o))
            return true;
    }

    return false;
}

Now when I call "In", the method will use "int" instead of "object", and no boxing will occur.  Additionally, since I used the generic IEnumerable<T> type, I will get some additional compile-time validation, so I won't be able to do things like this:

string[] values = {"5", "6", "10"};

bool isInArray = 7.In(values); // Compile time error!

I can now lean on the compiler to do some type checking for me.  What about some more interesting scenarios, where I want to extend some complex types?

Adding constraints to a generic extension method

I'd like to add some complex comparison operators to certain types, say something like "IsLessThanOrEqualTo".  I'd like to extend types that implement both IComparable<T> and IEquatable<T>.  That is, the extension method should only show up for types that implement both interfaces.  I can't declare both types with the "this" modifier parameter.  How can I accomplish this?  The trick is to make the method generic, and add constraints:

public static bool IsLessThanOrEqualTo<T>(this T lValue, T value)
    where T : IComparable<T>, IEquatable<T>
{
    return lValue.CompareTo(value) < 0 || lValue.Equals(value);
}

This extension method lets me write code such as:

int x = 5;
int y = 10;

bool isLte = x.IsLessThanOrEqualTo(y);
Assert.IsTrue(isLte);

This gives me a few advantages:

  • The available constraints are the same as any other generic type or method (struct, class, new(), <base class>, <interface>, and naked type constraints)
  • Using multiple constraints lets me constrain the method to types that fulfill all the constraints, such as the two interfaces in the above example
  • The signature of type T inside the generic method combines the members of all of the constraints.
    • All of the methods of IComparable<T> and IEquatable<T> are available to me in the above example.
  • The IDE filters the constraints in IntelliSense, and I won't even see extension methods whose constraints my variable doesn't fulfill

What this allows me to do in real world situations is to target specific scenarios by filtering through constraints.  Instead of using only 1 type in the "this" parameter, I can target objects that derive from a certain class, implement several interfaces, etc.  By making the class generic, interactions with the method will be strongly typed and will avoid boxing conversions and unnecessary casting.

To see the constraints fail, what happens when we try to call the IsLessThanOrEqualTo with a class that only partially fulfills the constraints?  Here's the bare-bones class:

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

I try to compile the following code:

Account account = new Account();
Account other = new Account();

account.IsLessThanOrEqualTo(other); // Compile time error!

It won't compile, since Account only implements IComparable<T>, and not IEquatable<T>.  This wouldn't happen while authoring the code, as IntelliSense doesn't even show the IsLessThanOrEqualTo method.

I should note that if I don't constrain the generic extension method, any instance of any type will have the method available for use, which may or may not be desirable.

Conclusion

Generic types and methods can be very helpful in eliminating boxing and unboxing as well as casting that clutters up the code.  Extension methods allow me to add functionality to types that I may not have access to changing.  By combining generic, constrained methods and extension methods, I get the power of extension methods with the flexibility of generic methods.

7 comments:

TexicanJoe said...

I don't think extension methods are part of 3.0 I believe they are only in 3.5. Other than that great stuff! Have you checked out Scott Bellware extenstions for NUnit?

http://codebetter.com/blogs/scott.bellware/archive/2006/12/18/156436.aspx

TexicanJoe said...

never mind I was wrong it is 3.0. I remember I didn't use them becuase the IDE (2005) does not support the language enhancements. Not to mention ReSharper.

Jimmy Bogard said...

Ah yes, bitten by the MS marketing team again. From what I understand, .NET Framework 3.5 uses C# 3.0 and VB9 and CLR 3.0. Too many versions.

I do really like NUnit.Spec though.

Gabe Moothart said...

I know I'm 3 months late, but I had to comment.

This is a really neat idea. From an API perspective, though, wouldn't it be better to have In<T> accept an array of T instead of IEnumerable<T>? This lets us make use of the params keyword and inline the array. Your example would become:
bool isInArray = 7.In("5", "6", "10");

and looses the array-creation baggage. We also lose some flexibility, but for me the primary use case of In<T> is with a series of literals rather than an existing collection .

Get blog, btw. Subscribed!

Jimmy Bogard said...

@Gabe

Yeah, that's something that's still lacking, a nicer collection initializer. C# 3.0 does have a collection initializer now:

List<int> numbers = new List<int> {0, 1, 2, 3};

Since a lot of new C# 3.0 features like LINQ deal exclusively with IEnumerable<T>, you would be giving up quite a bit to go with params instead. I dunno, params still needs some work I think, it would be cool if you could do params with IEnumerable<T> or something similar...

Gabe Moothart said...

@jimmy,
I think of In<T> as mostly useful for collapsing verbose logic:
x.In(5, 10, 15) replaces
(x==5) || (x==10) || (x==15)

If I already had an enumerable collection laying around and wanted to know if it contains some element x then I would probably search it using the existing LINQ extension methods, or maybe write a custom "contains" method. In that case, collection.contains(x) seems more straightforward to me than x.In(collection). But it could just be an issue of taste :-)

D. Patrick Caldwell said...

Hey, I really enjoyed your blog post. I've been writing a lot of extension methods lately myself. Sometimes you really have to be thankful to be a C# developer, you know?

Also, I was wondering what syntax highlighter you're using? I use one from google code and I have a pretty easy one liner for it. It serves me pretty well, but I'm not sure how well the community keeps up with the changes in c#