Skip to main content

C# Tutorial - Generics and the Where Clause [Intermediate]


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

Popular posts from this blog

C# Snippet - Shuffling a Dictionary [Beginner]

Randomizing something can be a daunting task, especially with all the algorithms out there. However, sometimes you just need to shuffle things up, in a simple, yet effective manner. Today we are going to take a quick look at an easy and simple way to randomize a dictionary, which is most likely something that you may be using in a complex application. The tricky thing about ordering dictionaries is that...well they are not ordered to begin with. Typically they are a chaotic collection of key/value pairs. There is no first element or last element, just elements. This is why it is a little tricky to randomize them. Before we get started, we need to build a quick dictionary. For this tutorial, we will be doing an extremely simple string/int dictionary, but rest assured the steps we take can be used for any kind of dictionary you can come up with, no matter what object types you use. Dictionary < String , int > origin = new Dictionary < string , int >();

C# Snippet - The Many Uses Of The Using Keyword [Beginner]

What is the first thing that pops into your mind when you think of the using keyword for C#? Probably those lines that always appear at the top of C# code files - the lines that import types from other namespaces into your code. But while that is the most common use of the using keyword, it is not the only one. Today we are going to take a look at the different uses of the using keyword and what they are useful for. The Using Directive There are two main categories of use for the using keyword - as a "Using Directive" and as a "Using Statement". The lines at the top of a C# file are directives, but that is not the only place they can go. They can also go inside of a namespace block, but they have to be before any other elements declared in the namespace (i.e., you can't add a using statement after a class declaration). Namespace Importing This is by far the most common use of the keyword - it is rare that you see a C# file that does not h

C# WPF Printing Part 2 - Pagination [Intermediate]

About two weeks ago, we had a tutorial here at SOTC on the basics of printing in WPF . It covered the standard stuff, like popping the print dialog, and what you needed to do to print visuals (both created in XAML and on the fly). But really, that's barely scratching the surface - any decent printing system in pretty much any application needs to be able to do a lot more than that. So today, we are going to take one more baby step forward into the world of printing - we are going to take a look at pagination. The main class that we will need to do pagination is the DocumentPaginator . I mentioned this class very briefly in the previous tutorial, but only in the context of the printing methods on PrintDialog , PrintVisual (which we focused on last time) and PrintDocument (which we will be focusing on today). This PrintDocument function takes a DocumentPaginator to print - and this is why we need to create one. Unfortunately, making a DocumentPaginator is not as easy as