views:

389

answers:

5

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?
+7  A: 

There really is no advantage to either option in terms of performance. It's really a matter of convenience and code maintainability. Choose the option you prefer. That being said, the methods in question limit your choices slightly.

You can use the IComparer<T> interface for List<T>.Sort, which would allow you to not duplicate code.

Unfortunately, BinarySearch does not implement an option using a Comparison<T>, so you cannot use a Comparison<T> delegate for that method (at least not directly).

If you really wanted to use Comparison<T> for both, you could make a generic IComparer<T> implementation that took a Comparison<T> delegate in its constructor, and implemented IComparer<T>.

public class ComparisonComparer<T> : IComparer<T>
{
    private Comparison<T> method;
    public ComparisonComparer(Comparison<T> comparison)
    {
       this.method = comparison;
    }

    public int Compare(T arg1, T arg2)
    {
        return method(arg1, arg2);
    }
}
Reed Copsey
A: 

In your case, advantage of having an IComparer<T> over Comparision<T> delegate, is that you can also use it for the Sort method, so you don't need a Comparison delegate version at all.

Another useful thing you can do is implementing a delegated IComparer<T> implementation like this:

public class DelegatedComparer<T> : IComparer<T>
{
  Func<T,T,int> _comparision;
  public DelegatedComparer(Func<T,T,int> comparision)
  {
    _comparision = comparision;
  }
  public int Compare(T a,T b) { return _comparision(a,b); }
}

list.Sort(new DelegatedComparer<Foo>((foo1,foo2)=>foo1.Bar.CompareTo(foo2.Bar));

and a more advanced version:

public class PropertyDelegatorComparer<TSource,TProjected> : DelegatedComparer<TSource>
{
  PropertyDelegatorComparer(Func<TSource,TProjected> projection)
    : base((a,b)=>projection(a).CompareTo(projection(b)))
}
George Polevoy
Typo: missing a closing curly bracket `}` at line 8 of first code snippet.
Ron Klein
+1  A: 

The delegate technique is very short (lambda expressions might be even shorter), so if shorter code is your goal, then this is an advantage.

However, implementing the IComparer (and its generic equivalent) makes your code more testable: you can add some unit testing to your comparing class/method.

Furthermore, you can reuse your comparer implementation when composing two or more comparers and combining them as a new comparer. Code reuse with anonymous delegates is harder to achieve.

So, to sum it up:

Anonymous Delegates: shorter (and perhaps cleaner) code

Explicit Implementation: testability and code reuse.

Ron Klein
I agree about the point on code reuse, however I'm not really convinced about testability. Why should a method accepting an `IComparer<T>` be any easier to test than one accepting a `Comparison<T>`? They both employ inversion of control.
Aaronaught
@Aaronaught, I think I'm misunderstood: **both** explicit implementations are easy to test (`IComparer<T>` and `Comparison<T>`), unlike anonymous delegates, which are harder to test.
Ron Klein
Ah, I understand, you're referring to unit-testing the `IComparer<T>` itself, not the method that accepts it. Can't imagine actually wanting to unit-test one of those, but you're right, it's definitely easier to write tests against if you want to.
Aaronaught
A: 

They really address different needs:

IComparable is useful for objects that are ordered. Real numbers should be comparable, but complex numbers cannot - it is ill-defined.

IComparer allows to define re-usable, well-encapsulated comparers. This is especially useful if the comparison needs to know some additional information. For example, you might want to compare dates and times from different time zones. That can be complicated, and a separate comparer should be used for this purpose.

A comparison method is made for simple comparison operations that are not complicated enough for reusability to be of any concern, e.g. sorting a list of customers by their first name. This is simple operation, hence does not need additional data. Likewise, this is not inherent to the object, because the objects are not naturally ordered in any way.

Lastly, there is IEquatable, which might be important if your Equals method can only decide if two objects are equal or not, but if there is no notion of 'larger' and 'smaller', e.g. complex numbers, or vectors in space.

mnemosyn
+5  A: 

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));
Aaronaught
Great code examples!
Kevin Crowell