.
Back when I was primarily a C++ developer, I loved C++ templates. The power of writing very reusable generic classes brought the art of programming to a brand new level.
Unfortunately, when .NET 1.0 came about, they didn’t have a template equivalent. With .NET 2.0 however, we finally got generics, which once again let us spread our wings and program more generically in the world of .NET
However, C# generics behave in some ways very differently from their C++ template cousins. There is a handy clause, however, that helps you navigate these waters to make your generics more powerful.
In C++, you can create a template and do nearly anything syntactically possible on the template parameter, and C++ will not check if the method/fields/operations invoked are valid until you declare a realization of the type. Let me illustrate with a C++ example:
1: // compiles fine, C++ makes no assumptions as to T
2: template <typename T>
3: class ReverseComparer
4: {
5: public:
6: int Compare(const T& lhs, const T& rhs)
7: {
8: return rhs.CompareTo(lhs);
9: }
10: };
Notice that we are invoking a method CompareTo() off of template type T. Because we don’t know at this point what type T is, C++ makes no assumptions and there are no errors.
C++ tends to take the path of not checking the template type usage until the method is actually invoked with a specific type, which differs from the behavior of C#:
1: // this will NOT compile! C# assumes lowest common denominator.
2: public class ReverseComparer<T>
3: {
4: public int Compare(T lhs, T rhs)
5: {
6: return lhs.CompareTo(rhs);
7: }
8: }
So why does C# give us a compiler error even when we don’t yet know what type T is? This is because C# took a different path in how they made generics. Unless you specify otherwise, for the purposes of the code inside the generic method, T is basically treated like an object (notice I didn’t say T is an object).
That means that any operations, fields, methods, properties, etc that you attempt to use of type T must be available at the lowest common denominator type: object.
Now, while object has the broadest applicability, it also has the fewest specific. So how do we allow our generic type placeholder to do things more than just what object can do?
So how do we get around this in C#? The answer is to constrain the generic type placeholder with the where clause. Basically, the where clause allows you to specify additional constraints on what the actual type used to fill the generic type placeholder must support.
You might think that narrowing the scope of a generic means a weaker generic. In reality, though it limits the number of types that can be used with the generic, it also gives the generic more power to deal with those types. In effect these constraints says that if the type meets the given constraint, you can perform the activities that pertain to that constraint with the generic placeholders.
One of the handiest where clause constraints is the ability to specify the type generic type must implement a certain interface or be inherited from a certain base class.
For example, you can’t call CompareTo() in our first C# generic without constraints, but if we constrain T to IComparable<T>, we can:
1: public class ReverseComparer<T>
2: where T : IComparable<T>
Now that we’ve constrained T to an implementation of IComparable<T>, this means that our variables of generic type T may now call any members specified in IComparable<T> as well. This means that the call to CompareTo() is now legal.
If you constrain your type, also, you will get compiler errors immediately if you attempt to use a type that doesn’t meet the constraint. This tends to be much clearer than the syntax error you would get within C++ template code itself when you used a type not supported by a C++ template.
Sometimes, you want to assign an instance of a generic type to null, but you can’t do this without constraints, because you have no guarantee that the type used to realize the generic is not a value type, where null is meaningless.
Well, we can fix this by specifying the class constraint in the where clause. By declaring that a generic type must be a class, we are saying that it is a reference type, and this allows us to assign null to instances of that type:
1: public static class ObjectExtensions
2: {
3: public static TOut Maybe<TIn, TOut>(this TIn value, Func<TIn, TOut> accessor)
4: where TOut : class
5: where TIn : class
6: {
7: return (value != null) ? accessor(value) : null;
8: }
9: }
In the example above, we want to be able to access a property off of a reference, and if that reference is null, pass the null on down the line. To do this, both the input type and the output type must be reference types (yes, nullable value types could also be considered applicable at a logical level, but there’s not a direct constraint for those).
Similarly to constraining a generic type to be a reference type, you can also constrain a generic type to be a value type. To do this you use the struct constraint which specifies that the generic type must be a value type (primitive, struct, enum, etc).
Consider the following method, that will convert anything that is IConvertible (int, double, string, etc) to the value type you specify, or null if the instance is null.
1: public static T? ConvertToNullable<T>(IConvertible value)
2: where T : struct
4: T? result = null;
5:
6: if (value != null)
8: result = (T)Convert.ChangeType(value, typeof(T));
10:
11: return result;
12: }
Because T was constrained to be a value type, we can use T? (System.Nullable<T>) where we could not do this if T was a reference type.
You can also constrain a type to require existence of a default constructor. Because by default C# doesn’t know what constructors a generic type placeholder does or does not have available, it can’t typically allow you to call one. That said, if you give it the new() constraint, it will mean that the type used to realize the generic type must have a default (no argument) constructor.
Let’s assume you have a generic adapter class that, given some mappings, will adapt an item from type TFrom to type TTo. Because it must create a new instance of type TTo in the process, we need to specify that TTo has a default constructor:
1: // Given a set of Action<TFrom,TTo> mappings will map TFrom to TTo
2: public class Adapter<TFrom, TTo> : IEnumerable<Action<TFrom, TTo>>
3: where TTo : class, new()
4: {
5: // The list of translations from TFrom to TTo
6: public List<Action<TFrom, TTo>> Translations { get; private set; }
7:
8: // Construct with empty translation and reverse translation sets.
9: public Adapter()
10: {
11: // did this instead of auto-properties to allow simple use of initializers
12: Translations = new List<Action<TFrom, TTo>>();
13: }
14:
15: // Add a translator to the collection, useful for initializer list
16: public void Add(Action<TFrom, TTo> translation)
17: {
18: Translations.Add(translation);
19: }
20:
21: // Add a translator that first checks a predicate to determine if the translation
22: // should be performed, then translates if the predicate returns true
23: public void Add(Predicate<TFrom> conditional, Action<TFrom, TTo> translation)
24: {
25: Translations.Add((from, to) =>
26: {
27: if (conditional(from))
28: {
29: translation(from, to);
30: }
31: });
32: }
33:
34: // Translates an object forward from TFrom object to TTo object.
35: public TTo Adapt(TFrom sourceObject)
36: {
37: var resultObject = new TTo();
38:
39: // Process each translation
40: Translations.ForEach(t => t(sourceObject, resultObject));
41:
42: return resultObject;
43: }
44:
45: // Returns an enumerator that iterates through the collection.
46: public IEnumerator<Action<TFrom, TTo>> GetEnumerator()
47: {
48: return Translations.GetEnumerator();
49: }
50:
51: // Returns an enumerator that iterates through a collection.
52: IEnumerator IEnumerable.GetEnumerator()
53: {
54: return GetEnumerator();
55: }
56: }
Notice, however, you can’t specify any other constructor, you can only specify that the type has a default (no argument) constructor.
The where clause is an excellent tool that gives your .NET generics even more power to perform tasks higher than just the base "object level" behavior.
There are a few things you cannot specify with constraints (currently) though:
In addition, you cannot overload a template definition with different, opposing constraints. For example you can’t define a Adapter<T> where T : struct and Adapter<T> where T : class.
Hopefully, in the future we will get some of these things to make the where clause even more useful, but until then what we have is extremely valuable in making our generics more user friendly and more powerful!
Print | posted on Thursday, January 13, 2011 6:29 PM |
Filed Under [
My Blog
C#
Software
.NET
Little Wonders
]