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:
1: public interface IComparer<in T>
2: {
3: // Compare two instances of same type.
4: int Compare(T x, T y);
5: }
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: // the opening price of the symbol quoted
4: public double? Open { get; set; }
5:
6: // ticker symbol
7: public string Symbol { get; set; }
8:
9: // etc.
10: }
11:
12: public class OpenPriceQuoteComparer : IComparer<Quote>
13: {
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: }
19: }
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.
Technorati Tags: C#,.NET,Little Wonders,BlackRabbitCoder,IComparable,Comparer