Search code examples
c#.netcomparisonicomparer

Advantages/Disadvantages of different implementations for Comparing Objects


This questions involves 2 different implementations of essentially the same code.

First, using delegate to create a Comparison method that can be used as a parameter when sorting a collection of objects:

class Foo
{
    public static Comparison<Foo> BarComparison = delegate(Foo foo1, Foo foo2)
    {
        return foo1.Bar.CompareTo(foo2.Bar);
    };
}

I use the above when I want to have a way of sorting a collection of Foo objects in a different way than my CompareTo function offers. For example:

List<Foo> fooList = new List<Foo>();
fooList.Sort(BarComparison);

Second, using IComparer:

public class BarComparer : IComparer<Foo>
{
    public int Compare(Foo foo1, Foo foo2)
    {
        return foo1.Bar.CompareTo(foo2.Bar);
    }
}

I use the above when I want to do a binary search for a Foo object in a collection of Foo objects. For example:

BarComparer comparer = new BarComparer();
List<Foo> fooList = new List<Foo>();
Foo foo = new Foo();
int index = fooList.BinarySearch(foo, comparer);

My questions are:

  • What are the advantages and disadvantages of each of these implementations?
  • What are some more ways to take advantage of each of these implementations?
  • Is there a way to combine these implementations in such a way that I do not need to duplicate the code?
  • Can I achieve both a binary search and an alternative collection sort using only 1 of these implementations?

Solution

  • Probably the biggest advantage to accepting a Comparison<T> as opposed to an IComparer<T> is the ability to write anonymous methods. If I have, let's say, a List<MyClass>, where MyClass contains an ID property that should be used for sorting, I can write:

    myList.Sort((c1, c2) => c1.ID.CompareTo(c2.ID));
    

    Which is a lot more convenient than having to write an entire IComparer<MyClass> implementation.

    I'm not sure that accepting an IComparer<T> really has any major advantages, except for compatibility with legacy code (including .NET Framework classes). The Comparer<T>.Default property is only really useful for primitive types; everything else usually requires extra work to code against.

    To avoid code duplication when I need to work with IComparer<T>, one thing I usually do is create a generic comparer, like this:

    public class AnonymousComparer<T> : IComparer<T>
    {
        private Comparison<T> comparison;
    
        public AnonymousComparer(Comparison<T> comparison)
        {
            if (comparison == null)
                throw new ArgumentNullException("comparison");
            this.comparison = comparison;
        }
    
        public int Compare(T x, T y)
        {
            return comparison(x, y);
        }
    }
    

    This allows writing code such as:

    myList.BinarySearch(item,
        new AnonymousComparer<MyClass>(x.ID.CompareTo(y.ID)));
    

    It's not exactly pretty, but it saves some time.

    Another useful class I have is this one:

    public class PropertyComparer<T, TProp> : IComparer<T>
        where TProp : IComparable
    {
        private Func<T, TProp> func;
    
        public PropertyComparer(Func<T, TProp> func)
        {
            if (func == null)
                throw new ArgumentNullException("func");
            this.func = func;
        }
    
        public int Compare(T x, T y)
        {
            TProp px = func(x);
            TProp py = func(y);
            return px.CompareTo(py);
        }
    }
    

    Which you can write code designed for IComparer<T> as:

    myList.BinarySearch(item, new PropertyComparer<MyClass, int>(c => c.ID));