James Michael Hare

...hare-brained ideas from the realm of software development...

News

Welcome to my blog! I'm a Sr. Software Development Engineer in the Seattle area, who has been performing C++/C#/Java development for over 20 years, but have definitely learned that there is always more to learn!

All thoughts and opinions expressed in my blog and my comments are my own and do not represent the thoughts of my employer.

C#/.NET Little Wonders: Comparer<T>.Default

I’ve been working with a wonderful team on a major release where I work, which has had the side-effect of occupying most of my spare time preparing, testing, and monitoring.  However, I do have this Little Wonder tidbit to offer today.

Introduction

The IComparable<T> interface is great for implementing a natural order for a data type.  It’s a very simple interface with a single method Compare() that compares two items of type T and returns an integer result.

So what do we expect for the integer return value?  It’s a pseudo-relative measure of the ordering of x and y, which returns an integer value in much the same way C++ returns an integer result from the strcmp() c-style string comparison function:

• If x == y, returns 0.
• If x > y, returns > 0 (often +1, but not guaranteed)
• If x < y, returns < 0 (often –1, but not guaranteed)

Notice that the comparison operator used to evaluate against zero should be the same comparison operator you’d use as the comparison operator between x and y.  That is, if you want to see if x > y you’d see if the result > 0.

The Problem: Comparing With null Can Be Messy

This gets tricky though when you have null arguments.  According to the MSDN, a null value should be considered equal to a null value, and a null value should be less than a non-null value.  So taking this into account we’d expect this instead:

• If x == y (or both null), return 0.
• If x > y (or y only is null), return > 0.
• If x < y (or x only is null), return < 0.

But here’s the problem – if x is null, what happens when we attempt to call CompareTo() off of x?

`   1: // what happens if x is null?`
`   2: x.CompareTo(y);`

It’s pretty obvious we’ll get a NullReferenceException here.  Now, we could guard against this before calling CompareTo():

`   1: int result;`
`   2:  `
`   3: // first check to see if lhs is null.`
`   4: if (x == null)`
`   5: {`
`   6:     // if lhs null, check rhs to decide on return value.`
`   7:     if (y == null)`
`   8:     {`
`   9:         result = 0;`
`  10:     }`
`  11:     else`
`  12:     {`
`  13:         result = -1;`
`  14:     }`
`  15: }`
`  16: else`
`  17: {`
`  18:     // CompareTo() should handle a null y correctly and return > 0 if so.`
`  19:     result = x.CompareTo(y);`
`  20: }`

Of course, we could shorten this with the ternary operator (?:), but even then it’s ugly repetitive code:

`   1: int result = (x == null)`
`   2:     ? ((y == null) ? 0 : -1)`
`   3:     : x.CompareTo(y);`

Fortunately, the null issues can be cleaned up by drafting in an external Comparer.

The Soltuion: Comparer<T>.Default

You can always develop your own instance of IComparer<T> for the job of comparing two items of the same type.  The nice thing about a IComparer is its is independent of the things you are comparing, so this makes it great for comparing in an alternative order to the natural order of items, or when one or both of the items may be null.

`   1: public class NullableIntComparer : IComparer<int?>`
`   2: {`
`   3:     public int Compare(int? x, int? y)`
`   4:     {`
`   5:         return (x == null)`
`   6:                    ? ((y == null) ? 0 : -1)`
`   7:                    : x.Value.CompareTo(y);`
`   8:     }`
`   9: }`

Now, if you want a custom sort -- especially on large-grained objects with different possible sort fields -- this is the best option you have.  But if you just want to take advantage of the natural ordering of the type, there is an easier way.

If the type you want to compare already implements IComparable<T> or if the type is System.Nullable<T> where T implements IComparable, there is a class in the System.Collections.Generic namespace called Comparer<T> which exposes a property called Default that will create a singleton that represents the default comparer for items of that type.  For example:

`   1: // compares integers`
`   2: var intComparer = Comparer<int>.Default;`
`   3:  `
`   4: // compares DateTime values`
`   5: var dateTimeComparer = Comparer<DateTime>.Default;`
`   6:  `
`   7: // compares nullable doubles using the null rules!`
`   8: var nullableDoubleComparer = Comparer<double?>.Default;`

This helps you avoid having to remember the messy null logic and makes it to compare objects where you don’t know if one or more of the values is null.

This works especially well when creating say an IComparer<T> implementation for a large-grained class that may or may not contain a field.  For example, let’s say you want to create a sorting comparer for a stock open price, but if the market the stock is trading in hasn’t opened yet, the open price will be null.  We could handle this (assuming a reasonable Quote definition) like:

`   1: public class Quote`
`   2: {`
`   3:     public double? Open { get; set; }  // opening price of symbol`
`   4:     public string Symbol { get; set; }  // the ticker symbol`
`   5:     // etc.`
`   6: }`
`   7:  `
`   8: // performing an external IComparer (could also implement IComparable<T>`
`   9: // in Quote instead if we have control over that class).`
`  10: public class OpenPriceQuoteComparer : IComparer<Quote>`
`  11: {`
`  12:     // Compares two quotes by opening price`
`  13:     public int Compare(Quote x, Quote y)`
`  14:     {`
`  15:         // Update: always make sure your arguments are not null `
`  16:         // I had left this off originally for brevity, but to avoid confusion...`
`  17:         // Note: ReferenceEquals checks if both same reference (or null)`
`  18:         if (ReferenceEquals(x, y)) return 0;`
`  19:         if (x == null) return -1;`
`  20:         if (y == null) return 1;`
`  21:  `
`  22:         // now can uses the Comparer<T>.Default to handle null-ness of the field`
`  23:         return Comparer<double?>.Default.Compare(x.Open, y.Open);`
`  24:     }`
`  25: }`

Summary

Defining a custom comparer is often needed for non-natural ordering or defining alternative orderings, but when you just want to compare two items that are IComparable<T> and account for null behavior, you can use the Comparer<T>.Default comparer generator and you’ll never have to worry about correct null value sorting again.

 Tweet Technorati Tags: C#, .NET, Little Wonders, IComparable, Comparer

Print | posted on Thursday, January 20, 2011 9:08 PM | Filed Under [ My Blog C# Software Little Wonders ]

#re: C#/.NET Little Wonders: Comparer<T>.Default

Good little tidbit to avoid null checking, but... your IComparer<Quote> example appears faulty. Won't this still throw a NullReferenceException if either x or y is null?

14: // Compares two quotes by opening price
15: public int Compare(Quote x, Quote y)
16: {
17: return Comparer<double?>.Default.Compare(x.Open, y.Open);
18: }

As soon as you reference x.Open or y.Open you'll get an exception!

The problem is you really *do* have to handle null checking. You are using Comparer<double?>.Default here to avoid null checking on double? when you need to be null checking x and y (the Quote objects).

Now, as you point out, you can use Comparer<Quote>.Default if you already have a IComparable<Quote> -- your code would then read:

14: // IComparer<Quote>
15: public int Compare(Quote x, Quote y)
16: {
17: return Comparer<Quote>.Default.Compare(x, y);
18: }

We are able to avoid null checking! BUT--- there's always a but --- You must still do null checking in your IComparable<Quote> implementation to avoid a NullReferenceException:

// IComparable<Quote>
public int CompareTo(Quote other)
{
if (other == null) return 1;
return Comparer<double?>.Default.Compare(this.Open, other.Open);
}

By implementing IComparable<Quote> we have gotten away with only a single null check instead of the ternary mess otherwise required, which you show as:

1: int result = (x == null)
2: ? ((y == null) ? 0 : -1)
3: : x.CompareTo(y);

This ternary null checking example misses testing y, which is okay since it should be handled in x.CompareTo(y), and it is in my example above.

The alternative is to provide a full implementation of Comparer<Quote> in which case we have to do all the null checking. I do it a bit different, which avoids the messy ternary operator:

if (x == y) return 0; // Handles BOTH null check and same object.
if (x == null) return -1;
if (y == null) return 1;

This code has the benefit of "short-circuiting" if x and y are the same object, and it handles both the x and y cases being null. But we don't need all this since we implemented IComparable<Quote> as I showed above.

Might as well throw in IComparable at this point:

// IComparable
public int CompareTo(object obj)
{
return this.CompareTo((Quote)obj);
}

Now we've covered IComparable, IComparable<Quote> and IComparer<Quote>!!

Thanks for teaching me about Comparer<T>.Default in the process!

Kevin
7/22/2011 8:53 AM | Kevin Rice

#re: C#/.NET Little Wonders: Comparer<T>.Default

By the way... the above really doesn't avoid any null checking. Using Comparer<T>.Default calls in a whole mess of code from the .NET Framework (look at the disassembled code with Reflector if you're curious). If you need FAST code over the convenience of Comparer<T>.Default then do the null checking yourself:

public class OpenPriceQuoteComparer : IComparer<Quote>
{
// Compares two quotes by opening price
public int Compare(Quote x, Quote y)
{
if (x == y) return 0; // Handles BOTH null check and same object.
if (x == null) return -1;
if (y == null) return 1;

return Comparer<Quote>.Default.Compare(x, y);
}
}

In this example I've only done null checking on Quote. We'd need another level of null checking to replace Comparer<double?>.Default as well.
7/22/2011 9:05 AM | Kevin Rice

#re: C#/.NET Little Wonders: Comparer<T>.Default

@Kevin: Very detailed comment, I must say.

1) I was keeping the Compare simple for illustration, yes you'd want to check for reference equality and null of the objects in question. I tend to make the assumption in my blog entries that folks will add their own null checking, though perhaps since this is so integral to the way Compare() works I should add it in as well.

2) The ternary doesn't miss null checking y because x.CompareTo(y) should handle it by definition (if implemented correctly)

3) Yes you'd want to obviously handle IComparable and IComparable<T> as well. Just keeping the example simple since my point was not how to write a full IComparable implementation (which would be a good fundamentals post) but to talk about Comparer<T>.Default isntead.

4) Comparer<T>.Default "calls a whole mess of code", actually this is a non-issue. It does the first time when it instantiates the singleton, but once it's loaded it's there and ready to go.
7/22/2011 9:59 AM | James Michael Hare

#re: C#/.NET Little Wonders: Comparer<T>.Default

1) Yes.
2) Yes (I acknowledged this point).
3) Yep.
4) Excellent point. Didn't think of that. An instantiated Nullable<T>.Compare(x,y) looks like this (using Reflector):

public override int Compare(T? x, T? y)
{
if (x.HasValue)
{
if (y.HasValue)
{
return x.value.CompareTo(y.value);
}
return 1;
}
if (y.HasValue)
{
return -1;
}
return 0;
}

This code is "halfway between" your ternary code and my "short-circuit" code---it does more than your ternary code since it detects y=null instead of calling CompareTo(y), but does "less" than my code because it doesn't contain my short-circuit trick which also eliminates the CompareTo(y) if x=y.

But you are very correct that Default() only instantiates the singleton once (it is stored in a private static field), thus multiple calls to Default() are not expensive as I proclaimed.

I always learn a bundle from a good discourse! Thanks a ton for allowing me to learn and experiment with your code and article!
7/22/2011 3:07 PM | Kevin Rice

#re: C#/.NET Little Wonders: Comparer<T>.Default

@Kevin: Me too, the moment we stop learning we stop being useful :-) I still am amazed by how much I still have yet to learn even after 18 years of software development.
7/24/2011 5:11 PM | James Michael Hare