A Simple Case For C# Generic Constraints

Tuesday, June 18, 2013

Imagine you need to support multiple projects that retrieve “widgets” of different types from various data sources. You might start with some simple type definitions that build a core API around widgets and widget storage.

public class Widget
{
    public int Key { get; set; }        
}

public interface IWidgetStore
{
    Widget GetWidget(int key);
}

The IWidgetStore interface is needed so that reusable algorithms can work with widgets.

Given these definitions, someone can build a widget specific for their project.

public class NamedWidget  : Widget
{        
    public string Name { get; set; }
}

As well as a concrete class for storing widgets.

public class WidgetStore : IWidgetStore
{
    public Widget GetWidget(int key)
    {          
        return _store.Single(u => u.Key == key);
    }
    
    readonly List<NamedWidget> _store = new List<NamedWidget>();
}

But the friction in development will start because specific projects don’t want to work with Widget, they want to work with NamedWidget where business properties like Name are defined. If every IWidgetStore can only return Widget, every caller has to cast the return value of GetWidget to a type like NamedWidget.

var widget = (NamedWidget)store.GetWidget(id);
var name = widget.Name;

One solution is to use an explicit interface definition to hide the IWidgetStore from application developers.

public class WidgetStore : IWidgetStore
{
    public NamedWidget GetWidget(int key)
    {            
        return _store.Single(u => u.Key == key);
    }

    Widget IWidgetStore.GetWidget(int key)
    {
        return GetWidget(key);
    }

    readonly List<NamedWidget> _store = new List<NamedWidget>();
}

Now the application code can work with NamedWidget and still pass the WidgetStore to other low level algorithms which will access the object through the IWidgetStore interface.

But, if the application is trying to stay flexible or testable it might want to continue working with widget stores through the IWidgetStore interface too, just like the reusable algorithms, and then the code would still need to cast the return value of GetWidget.

Another solution is to use a generic type parameter to parameterize a widget store with the desired type of widget.

public interface IWidgetStore<TWidget>
{
    TWidget GetWidget(int key);
}

Now a concrete widget store can implement the interface and return derived Widget objects.

public class WidgetStore<TWidget> : 
    IWidgetStore<TWidget>
    where TWidget: Widget
{
    public TWidget GetWidget(int key)
    {
        return _store.Single(u => u.Key == key);
    }

    readonly List<TWidget> _store = new List<TWidget>();
}

What makes it all work is the generic constraint where TWidget: Widget. The C# complier assumes a TWidget is of type Object unless told otherwise, so the LINQ query in GetWidget will fail with a compiler error because Object doesn’t have a Key property.

The generic constraint tells the C# compiler that a TWidget has to be a Widget, and since a Widget has to have a Key property the C# compiler is happy to compile the code.


Comments
gravatar Mike Wednesday, June 19, 2013
Scott, appreciate the post. The calling code will create a WidgetStore to handle instances of any Widget subclass so am wondering if this use of generic constraints illustrates any particular 'software pattern' or OO practice? Brain is trying to "file under" something other than just generics and testability. cheers.
gravatar Scott Wednesday, June 19, 2013
@Mike: I'm not aware of a pattern name, actually. I was asked a question about this and came up with a scenario I've repeated many times over the years, so there should be a pattern name. Perhaps "parametric polymorphism" is the closest term I can think of. http://msdn.microsoft.com/en-us/magazine/hh148146.aspx
gravatar Siim Wednesday, June 26, 2013
Another option is to use just interface IWidgetStore and generic method under that: TWidget GetWidget/TWidget/(int key). This way you have one store for all the widgets. But it all depends on the use case.
Comments are now closed.
by K. Scott Allen K.Scott Allen
My Pluralsight Courses
The Podcast!