Search
Close this search box.

C#/.NET Little Wonders: Comparer.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 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.

Print | posted on Thursday, January 20, 2011 9:08 PM | Filed Under [ My BlogC#SoftwareLittle Wonders ]

This article is part of the GWB Archives. Original Author: james Michael Hare

Related Posts