Advantages/Disadvantages of different implementations for Comparing Objects

我的未来我决定 提交于 2019-12-03 01:40:36

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));

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);
    }
}

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.

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.

George Polevoy

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)))
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!