Making the most of generic type inference

Introduction


Specifying type arguments for generic types and methods can be a pain, especially when there are multiple type parameters involved. For instance, imagine having to explicitly specify TOuter, TInner, TKey and TResult for a call to Enumerable.Join! Fortunately the compiler can work out the type arguments most of the time – but only for generic methods. It doesn’t do anything for generic types. However, all is not lost…


Overloading type names by number of type parameters


One feature of C# and .NET which isn’t used terribly often (in my experience) is the ability to use the same type name for different types – so long as they have different numbers of type parameters. This is how System.Nullable and System.Nullable<T> coexist, for example. Like all language features this is open to massive abuse if you have several types with very different semantic meanings, but when applied with discretion it can be very helpful.


We can use this to our advantage when we want to call a constructor or static method of a generic type without specifying the type parameters. The basic idea is that you create a nongeneric type (or just one with fewer type parameters) and then put a generic method in that class. The generic method in the nongeneric class then calls a member in the generic class, using the method’s type parameters as the type arguments for the generic class. (Try getting all of that right after a few drinks!) Now that you’ve got a generic method, you can use type inference to avoid having to explicitly state the type arguments. Fortunately it’s a lot simpler than it sounds…


To show you what I mean, let’s look at a bit of code from my MiscUtil library. (This code isn’t in the latest release drop, but will be in the next one – and this post provides all the important code anyway.)


Projection comparisons


I’ve found the OrderBy method in LINQ very useful, and I wanted to be able to use the same “compare using a projection” idea elsewhere. The IComparer<T> interface is used in various places in the .NET API (List<T>.Sort being an obvious example) but implementing it can be a bit tedious – even though it’s a single method. So, let’s build a ProjectionComparer type which knows how to compare two objects by applying the same projection to both of them, and then using another comparer to compare the results.


There are two types involved – the source of the projection, and the key we’re projecting it to. This naturally suggests a type with two type parameters, TSource and TKey. For instance, when projecting from a Person type to their name, we might have TSource=Person and TKey=string.


The most obvious piece of information we need to create a projection comparer is the projection itself. A delegate is the obvious way of representing this – a Func<TSource, TTarget> which can be applied to each item we try to compare. We then need to know how to compare the names (e.g. case-insensitive, ordinal etc) – functionality which is provided by StringComparer in this example, and IComparer<TKey> in general. The Comparer<T>.Default property comes in handy to let us get away without specifying a comparer in many situations.


With those few design decisions, we can implement ProjectionComparer<TSource, TKey> pretty simply:


 


public class ProjectionComparer<TSource, TKey> : IComparer<TSource>
{
    private readonly Func<TSource, TKey> projection;
    private readonly IComparer<TKey> comparer;

    public ProjectionComparer(Func<TSource, TKey> projection)
        : this (projection, null)
    {
    }
       
    public ProjectionComparer(Func<TSource, TKey> projection, IComparer<TKey> comparer)
    {
        if (projection==null)
        {
            throw new ArgumentNullException(“projection”);
        }
        this.comparer = comparer ?? Comparer<TKey>.Default;
        this.projection = projection;
    }

    public int Compare(TSource x, TSource y)
    {
        // Don’t want to project from nullity
        if (x==null && y==null)
        {
            return 0;
        }
        if (x==null)
        {
            return -1;
        }
        if (y==null)
        {
            return 1;
        }
        return comparer.Compare(projection(x), projection(y));
    }
}

 


That’s functionally complete, but it’s a bit of a pain to create instances of it. Our previous example would require something like this:


 


var nameComparer = new ProjectionComparer<Person, string>(person => person.Name);

 


It’s not bad, but we can do better.


Introducing the nongeneric ProjectionComparer type


The next step is almost as simple as imagining how we want to create instances. We don’t have to use a nongeneric type with the same name as the generic type, but it keeps things consistent, and forms a simple pattern to follow at other times. So, let’s imagine being able to write this:


 


var nameComparer = ProjectionComparer.Create(person => person.Name);

 


Unfortunately we can’t quite achieve that. There’s no way for the compiler to know the type of the parameter in the lambda expression. However, we have three options we can use:


 


// Explicitly type the lambda expression’s parameter
var option1 = ProjectionComparer.Create((Person person) => person.Name);

// Pass in a dummy parameter of the right type
var option2 = ProjectionComparer.Create(dummyPerson, person => person.Name);

// Use a class with one generic type parameter, and infer the other
var option3 = ProjectionComparer<Person>.Create(person => person.Name);

 


Each of these options is just a way of telling the compiler what TSource should be. The first two are implemented in a totally nongeneric class. The third is implemented in a generic class with a type parameter for TSource but letting the compiler infer TKey. Note that we have to make this split because you can’t explicitly specify some type arguments and let the compiler infer the others. The actual code for these methods is very straightforward indeed. I haven’t included overloads where the comparer is explicitly specified, but it’s very simple to do so if required.


 


public static class ProjectionComparer
{
    // For option 1
    public static ProjectionComparer<TSource, TKey> Create<TSource, TKey>(Func<TSource, TKey> projection)
    {
        return new ProjectionComparer<TSource, TKey>(projection);
    }

    // For option 2
    public static ProjectionComparer<TSource, TKey> Create<TSource, TKey>(TSource ignored, Func<TSource, TKey> projection)
    {
        return new ProjectionComparer<TSource, TKey>(projection);
    }

}

// For option 3
public static class ProjectionComparer<TSource>
{    
    public static ProjectionComparer<TSource, TKey> Create<TKey>(Func<TSource, TKey> projection)
    {
        return new ProjectionComparer<TSource, TKey>(projection);
    }
}

 


Conclusion


There’s nothing particularly difficult in this post, but it’s sometimes easy to forget that the C# compiler can help you out when it comes to filling in type arguments. Of course it only helps when you already providing enough information to the compiler with normal method parameters, but it’s still a nice little trick to have up your sleeve when you’re trying to make your APIs that bit more pleasant to use.

2 thoughts on “Making the most of generic type inference”

  1. You can take this one step further: combine option 1 or 3 with an extension method to create the ProjectionComparer for you, inferring all of the type arguments.

    public static class ProjectionExtensions
    {
    public static void SortByProjection(this List source, Func projection)
    {
    // Option 1
    source.Sort(ProjectionComparer.Create
    (projection));
    // Option 3
    source.Sort(ProjectionComparer
    .Create(projection));
    }
    }

  2. @David: Yes, extension methods can help if you know where you’re going to use the comparer.

    Another extension method I’ve found useful is on IComparer itself – a ThenBy method (and ThenByDescending) which does the obvious thing. Again, there’s less type inference required because you already know the source type at that point.

    Jon

Comments are closed.