If you write C# on a daily basis, chances are that you use generic
classes and functions on a daily basis as well (really, how could you
not!). Using generic classes/functions is really easy - but the flip
side of that coin, writing generic classes/functions, can be difficult.
Today we are going to look at one of the tools in your arsenal for
writing generic classes and functions - the
where
clause.
So when you are writing a generic class or method, by default that class
or method can be used with any C# type - reference type, value type,
random type from some other person's code - it could be anything. Now,
sometimes, this is fine - you really don't care what the type will be -
but other times you might like the list of possibilities to be somewhat
more finite. This is where the
where
clause
comes into play. It allows you to put various types of constraints on
the type parameter for a generic method or function.
Take a look at the following example of a generic function:
public static T FindVisualParent<T>(DependencyObject obj)
where T : DependencyObject
{
if (obj == null)
return null;
T correctlyTyped = obj as T;
if (obj != null)
return correctlyTyped;
if (obj is Visual)
{
obj = VisualTreeHelper.GetParent(obj);
}
else
{
FrameworkContentElement fce = obj as FrameworkContentElement;
if (fce != null)
obj = fce.Parent;
else
throw new ArgumentException(
"Cannot Walk Parent Tree of " + obj.GetType().ToString());
}
return FindVisualParent<T>(obj);
}
This is a very handy (in my opinion) utility function for WPF that
finding the first parent of the given type by walking up the visual tree
from the given dependency object. But we don't really care about the
utility at the moment - we care about the generics. So, as you probably
saw, there is a
where
clause at the end of the function definition.
This constrains the type T to be something derived from
DependencyObject
. This is extremely useful in this case because it
doesn't make sense to try and find something in the parent tree that is
not a DependencyObject
.
There is another thing that having a constraint gets us in this case -
we are able to return null out of the function. That's right, if we
didn't have a constraint, we couldn't return null - because for some
values of T, null would not be a valid value (remember - someone could
pass a value type in). But because
DependencyObject
is an object (and
therefore a reference type), we are allowed to return null.
Before we move on to more complicated examples, let's take a look at the
various types of possible constraints.
where T: struct
This constrains T to value types (things like ints, bools, and more
complicated structs) - pretty simple.
where T : class
This constrains T to reference types. Doing this allows you to use
things like null.
This guarantees that T will have a public constructor that takes no
arguments - generally you use this constraint if you will need to create
a new instance of the type.
where T : \
We already saw this one - it is what we were using in the above example.
Using this, you are also automatically constraining the type to be a
reference type. However, you are not necessarily guaranteeing that you
get the ability to create new instances (even if the base class has a
public no-argument constructor - because a sub class might not).
where T : \
This type of constraint will guarantee that T implements the given
interface. Because both classes and structs can implement interfaces,
this does not constrain T to be a reference or value type.
where T : R
This means that the type supplied for T must be the same as or derive
from the type supplied for R.
Ok, time for some more examples. Take a look at this handy extension
method for IEnumerable, which puts two constraints at once on one of the
types:
public static TCollection ConvertAll<TInput, TOutput, TCollection>(
this IEnumerable<TInput> input, Converter<TInput, TOutput> convert)
where TCollection : ICollection<TOutput>, new()
{
TCollection output = new TCollection();
foreach (TInput item in input)
output.Add(convert(item));
return output;
}
This method takes an IEnumerable full of instances of TInput, converts
them all to instances of TOuput, and adds these new instances to a
collection of type TCollection, which is then returned. There are two
constraints here on TCollection. First, that it implements
ICollection<TOutput>
- this makes sure that we can add instances of
TOuput to the collection. And second, that we can create new instance of
this collection (since all we are doing is requiring an interface, we
have to call this out separately).
Below is some sample code showing how this function can be used:
var myDoubles = new Stack<double>();
myDoubles.Push(1.1);
myDoubles.Push(2.1);
myDoubles.Push(1.8);
myDoubles.Push(2.9);
var myInts = myDoubles.ConvertAll<double, int, LinkedList<int>>(
val => (int)Math.Round(val));
//myInts now has 3, 2, 2, 1 in it
Ok, now we have seen how to put multiple constraints on a single type
(you just separate them by commas), lets take a look at how to put
constraints on multiple different types. Take a look at the following:
public class MyClass<T, R> :
where T : class, IComparable
where R : new()
{
//My Class Code
}
It is that simple, all you need to do is list them out like that, one
where
clause right after another. I did a class here just to show
generics on a class, but is the exact same thing for functions (and
everything that we have talked about above for functions also applies to
generic classes).
A couple other random things to note here about constraints. One is that
you can't put conflicting constraints on a type - you can't ask for both
class
and struct
. Another is that you can't use a sealed class or an
actual value type as a type constraint - it wouldn't really make sense
anyway, because you can't derive from them (so there is only ever one
exact type that will match). Oh, and for some reason, if you are using a
new()
constraint, it always needs to be the last constraint in the
list for that type. I haven't seen a reason as to why that is the case -
but you get a compile error if you don't.
And that is it for the
where
clause and what you can do with it. I
recommend adding constraints when it does make sense - it makes thinking
about the code you are writing a good bit easier, especially just by
constraining to reference or value types.
Comments
Post a Comment