Design Journey Into Searches, Sorts, and IComparer

Friday, July 30, 2004
Picture this scenario: you have an array of objects and need to search through the array for an object matching a specific ID. You know the array will be around for a long time in your application, so you need to make the search quick. You know you do not want to implement the search with the following code, because each search could potentially loop through the entire array of objects:

SomeClass result = null;
foreach(SomeClass c in someCollection)
{
   if(c.ID == "A123")
   {
      result = c;
      break;
   }
}
return result;

You know the Array class in .NET has a BinarySearch method which could improve the speed of your search tremendously, but in order for a binary search to work the array has to be sorted. Let’s start here.

First Try With Array.Sort

Let’s select a class from the framework to use in the code samples which will represent SomeClass in the above pseudo-code. I’ve decided to use the CultureInfo class from System.Globalization for two reasons. First, it is easy to retrieve an array of CultureInfo objects with the GetCultures static method on the CultureInfo class. Secondly, CultureInfo is a non-trivial class having properties to represent a culture’s name, calendar, writing system, date format, and more. Let’s try to quickly sort an array of CultureInfo objects:

CultureInfo[] cultures = CultureInfo.GetCultures(CultureTypes.AllCultures);
Array.Sort(cultures);

The first line of code will populate an array of 202 CultureInfo objects representing cultures from Afrikaans to Vietnamese. The second line of code throws an exception:

System.ArgumentException: At least one object must implement IComparable.

We’ve asked for the Array class to perform a sort, but it is telling us it does not know how to compare two CultureInfo objects. After all, CultureInfo is not a simple string - the class has many properties and the Sort method does not know which one to choose. The framework designers gave us no built in technique to compare two CultureInfo objects so we will need to define our own way to order the objects.(If you are the author of the class you need to sort, you can inherit from the IComparable interface and implement the CompareTo method in your class).

 

We know we want to compare the Name properties of the CultureInfo objects for them to sort properly. Example names include "en-CA" for a culture of English – Canada, and "fr-CA" for French – Canada. Let’s try to implement this next.

Array.Sort with an IComparer

There is an overloaded version of the Sort method which lets us pass an object telling the Sort method how to compare. The object must implement the IComparer interface, which specifies one single method to implement: Compare. Compare accepts two object parameters: x and y. If x is less then y we must return a negative number, if x equals y we must return 0, and if x is greater than y we must return a positive number. Let’s try another iteration of our software.

First, we will define our class implementing IComparer:

class CultureComparer : IComparer
{   
   public int Compare(object x, object y)
   {        
      CultureInfo a, b;

      a = x as CultureInfo;
      b = y as CultureInfo;

      return a.Name.CompareTo(b.Name);
   }
}

Notice how we let the ComapeTo method of the string object compare the names. Notice also that we assume both parameters are CultureInfo objects, if someone passes an argument that is not a CultureInfo object, the runtime will throw an exception. Now let’s put the class to work.

CultureInfo[] cultures = CultureInfo.GetCultures(CultureTypes.AllCultures);
Array.Sort(cultures, new CultureComparer());

The above code will complete successfully. All we have left is to search for a specific culture. What do you expect would happen if we added the following code?

Array.BinarySearch(cultures, "fr-CA", new CultureComparer()); 

Unfortunately, what we will have is an exception of type System.NullReferenceException. When we asked the array to sort the array would use the Compare method to compare two CultureInfo objects. When we ask the array to search for the culture “en-FR”, it now has to compare a CultureInfo object with a string. Trying to cast the second parameter from object to CultureInfo will fail and return a null reference, and the code inside of Compare throws an exception when we try to use the null reference. We need to refine the code to handle both cases.

Sorting and Searching With a Comparer Helper

Before we jump in and try to fix the code, let’s take a step back and review some other points. First, creating a new object each time we want to do a search seems like unnecessary overhead. A single instance of the comparer object for all searches would suit just fine, because the method is stateless and is be multi-threaded safe. Secondly, we could fix the existing Compare method with a simple check:

if(y is String)
{
   // compare CultureInfo to string
}
else if(y is CultureInfo)
{
   // compare two CultureInfo objects
}

Type checking can be expensive in tight loops for searching and sorting, and although we could guess and say most of the time the y parameter will be a string, we don’t know how the class might be used in the future. Instead of using an if else, we will create a comparer class for each scenario.

Our plan is to wrap up all of the functionality we need into a single class. The single class will manage the comparer objects we need:

sealed class CultureComparerHelper
{            
   static public IComparer CultureComparer
   {
      get { return _cultureComparer; }
   }

   static public IComparer StringComparer
   {
      get { return _stringComparer; }
   }

   private static PrivateCultureComparer _cultureComparer = new PrivateCultureComparer();
   private static PrivateStringComparer _stringComparer = new PrivateStringComparer();

   private class PrivateCultureComparer : IComparer
   {
      public int Compare(object x, object y)
      {
         return ((CultureInfo)x).Name.CompareTo(((CultureInfo)y).Name);
      }
   }

   private class PrivateStringComparer : IComparer
   {         
      public int Compare(object x, object y)
      {
         return ((CultureInfo)x).Name.CompareTo((string)y);
      }
   }
}

First notice the class is sealed. Since all of the properties in this class are static, there is no advantage in inheriting from the class. Secondly, notice the properties to retrieve the comparer objects (StringComparer, CultureComparer) return a type of IComparer instead of the actual type of the object. This is all a client needs to know about the comparer, that it implements IComparer, any additional information is unnecessary and we are now free to make internal changes to the class in the future. This is an important point to make when developing with interfaces.

Since the ultimate type of the comparer objects are unnecessary for anyone using the class to know, we have made them private nested types of the helper class. Nobody outside of the CultureComparerHelper class will ever be able to see or instantiate the PrivateCultureComparer class nor the PrivateStringComparer class. Someday in the future we could swap these classes out, or combine them into a single class with the ability to compare both CultureInfo and string objects, and the code using the helper class will never need to change – a great advantage to the abstraction we’ve built.

Finally, let’s take a look at a working code sample to sort and then search an array of CultureInfo objects.

CultureInfo[] cultures = CultureInfo.GetCultures(CultureTypes.AllCultures);

Array.Sort(cultures, CultureComparerHelper.CultureComparer); 

int index = Array.BinarySearch(cultures, "en-CA", CultureComparerHelper.StringComparer);

Conclusion

In this article you might have learned a bit about how IComparer works, but hopefully also you’ll take away an understanding of the power of interfaces and abstracting away details in a design.

-- K. Scott Allen

by K. Scott Allen K.Scott Allen
My Pluralsight Courses
The Podcast!