C# Corner

Create Custom Constraints

Learn how to construct a generic class that mandates behavior from type parameters that aren't expressible in the standard constraint types.

Technology Toolbox: VB.NET, C#, SQL Server 2005 SP2, ASP.NET, XML, VB6, Other

When you create Generic types, or generic methods, you can use constraints to enforce certain assumptions on the type parameters. For example, this method can be used only with types that implement IComparable<T>:

public static bool GreaterThan<T>(this T left,
    T right) 
    where T : IComparable<T>
{
    if (left == null)
         return false;
    return left.CompareTo(right) > 0;
}

Constraints are enforced at compile time, so it's impossible for someone to misuse this method by calling it with an incompatible type.

The possible sets of constraints are quite large, but there are limitations. You can specify a base class, any set of interfaces, and three special constraints: new(), class, and struct. But, what if you need to specify some other method or interface in a class that doesn't match those constraints?

In this column, I'll show you how to construct a generic class that mandates behavior from type parameters that aren't expressible in the standard constraint types. You'll learn how to recognize this pattern when you come across it and learn how to create generics that use this same pattern.

Let's start with a class example. Assume that you want to create a generic class that reads text from a stream and constructs a sequence of similarly typed objects:

public class TextStreamFactory<T>
{
    private TextReader objectReader;

    public TextStreamFactory(
       TextReader reader)
    {
         objectReader = reader;
    }

    public IEnumerable<T> 
		CreateSequence()
    {
         string lineFromStream = 
             objectReader.ReadLine();
         while (lineFromStream != null)
         {
             // won't compile, but it's what we   
             // want to do:

             yield return new T(
                  lineFromStream);
             lineFromStream = 
                  objectReader.ReadLine();
         }
    }
}

Unfortunately, the code depicted in this listing doesn't work because you can't specify a constraint for an arbitrary constructor. The only constructor constraint you're allowed to specify is that a parameterless constructor exists.

But you don't want to give up this idea yet. There's a way to abstract away the constraint and let this particular generic class work. All you need to do is create a way to specify some other constructor for the factory. In general, you can specify any constraint using a delegate definition. The next step is to modify the code that wouldn't compile earlier so that any user can specify the factory method or constructor when using the class:

public class TextStreamFactory<T>
{
    public delegate T Factory(
         string textRepresentation);

    private TextReader objectReader;

    public TextStreamFactory(
         TextReader reader)
    {
         objectReader = reader;
    }

    public IEnumerable<T> 
         CreateSequence(Factory 
		constructor)
    {
         string lineFromStream = 
             objectReader.ReadLine();
         while (lineFromStream != null)
         {
             yield return constructor(
                  lineFromStream);
            lineFromStream = 
                objectReader.ReadLine();
         }
    }
}

The key thing is to remember that any arbitrary method can be expressed as a delegate. Here, you need a method that takes a string as a parameter and returns an object of type T:

public delegate T Factory(
	string textRepresentation);

You can use any method that can be used as the target of a Factory delegate to enable the CreateSequence() to do its work. For example, assume you want to read a set of names from the console. You could start with a simple person type:

public class Person
{
    public string LastName
    {
         get;
         set;
    }

    public string FirstName
    {
         get;
         set;
    }
}

This type has two simple properties: a first name and a last name. You need to create a method that takes a string input and returns a Person object to use the type with the TextStreamFactory class. This is a simple static method you can add to the Person type:

public static Person CreatePerson(
    string text)
{
    // Split on spaces:
    string[] tokens = text.Split(' ');
    if (tokens.Length != 2)
         throw new 
             InvalidOperationException(
             "Too many tokens for a person");
    return new Person 
         { LastName = tokens[1], 
             FirstName = tokens[0] };
}

You can use the Person type with an input stream once you add this method to your Person type. I wrote a simple application to test this by reading names from the console. These three lines of code add a set of people to a list collection (see Listing 1 for the complete listing):

TextStreamFactory<Person> Builder = new 
    TextStreamFactory<Person>(Console.In);
List<Person> peeps = new List<Person>();
peeps.AddRange(Builder.CreateSequence(
    s => Person.CreatePerson(s)));

There are several advantages to using this kind of technique. As I mentioned earlier, any arbitrary method can be expressed as some kind of a delegate. This means you can specify any possible needs for your type parameters as a delegate. You are no longer bound by the formal constraints that you can express in the where portion of a generic class definition.

Give Users Flexibility
The users of your generic class gain a lot of flexibility, as well. When you specify constraints in the form of base classes or interfaces, there's only one way for classes used as type parameters to satisfy those constraints. When you specify your expectations using delegate definitions, class implementers have more discretion over how to satisfy those constraints. The sample in this article relies on a static method that is a member of the person class. But, that's not a requirement. You could also use an instance member function, or you could use a method in another class. You could also write the code inline as an anonymous method or lambda expression.

The important point is that the generic class specifies only the minimum requirements to accomplish its work. In this case, the minimum requirement is that a method exists to create an object from a string. How that contract is satisfied is completely up to what implements the class.

You should also see that this technique allows class implementers to use only part of a generic class. For example, you might show only one method to create the sequence, as in the example discussed in this article. But it's just as likely that you would write code to generate the text from a series of objects. For example, you might add this code to the TextStreamFactory<T> class:

public delegate string 
    TextRepresentation(T theObject);
public void WriteSequence(
    TextWriter outputStream, 
    IEnumerable<T> sequence,
    TextRepresentation 
		outputFunction) 
{
    foreach (
    T item in sequence) 
    {
         outputStream.WriteLine(
             outputFunction(item));
    }
}

The original sample still compiles and runs. You haven't implemented a method that satisfies WriteSequence(), but it doesn't matter. You don't call that method of the generic class, so you don't need to create an implementation for it.

In any case, the techniques I've shown in this article allow you to specify any arbitrary functionality in the code that uses a generic class or method. Use this technique when you need to define requirements of class implementers that aren't possible using the normal constraint mechanism. In particular, methods like operators, arbitrary constructors, or other operations can be specified using delegate definitions. That opens the door to allowing implementers to satisfy those constraints using any code they can create. It's much more open.

About the Author

Bill Wagner, author of Effective C#, has been a commercial software developer for the past 20 years. He is a Microsoft Regional Director and a Visual C# MVP. His interests include the C# language, the .NET Framework and software design. Reach Bill at wwagner@srtsolutions.com.

comments powered by Disqus
Upcoming Events

.NET Insight

Sign up for our newsletter.

I agree to this site's Privacy Policy.