不同实现比较对象的优缺点

18

这个问题涉及两种本质上相同的代码实现。

首先,使用委托创建一个比较方法,该方法可以用作对对象集合进行排序时的参数:

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

当我想以不同于我的 CompareTo 函数所提供的方式对一组 Foo 对象进行排序时,我使用上述方法。例如:

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

其次,使用 IComparer 接口:

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

当我想在一个Foo对象的集合中进行二分搜索时,我使用上述方法。例如:

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

我的问题是:

  • 这些实现的优缺点是什么?
  • 还有哪些方法可以利用这些实现的优势?
  • 是否有一种方式可以将这些实现组合起来,以不需要重复代码的方式进行?
  • 我能否只使用其中一种实现来实现二分查找和另一种集合排序的功能?
5个回答

7

就性能而言,两种选项都没有优劣之分。这只是方便和代码可维护性的问题。选择你喜欢的选项即可。话虽如此,所讨论的方法稍微有些限制。

你可以使用 IComparer<T> 接口来对 List<T>.Sort 进行操作,这样可以避免重复编写代码。

不幸的是,BinarySearch 没有实现使用 Comparison<T> 的选项,因此你不能直接为该方法使用 Comparison<T> 委托。

如果你真的想要同时使用 Comparison<T>,你可以创建一个泛型 IComparer<T> 实现,在其构造函数中接受一个 Comparison<T> 委托,并实现 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);
    }
}

6

接受Comparison<T>而不是IComparer<T>的最大优势可能是能够编写匿名方法。比如我有一个List<MyClass>,其中MyClass包含一个应用于排序的ID属性,我可以这样写:

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

这比编写整个IComparer<MyClass>实现要方便得多。

我不确定接受IComparer<T>是否真的有任何主要优势,除了与遗留代码(包括.NET Framework类)兼容。 Comparer<T>.Default属性仅对原始类型真正有用;其他所有类型通常需要额外的编码工作来对抗。

为了避免在需要使用IComparer<T>时出现代码重复,我通常会创建一个通用的比较器,例如:

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

这使得编写以下代码成为可能:

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

这不一定漂亮,但可以节省一些时间。

我还有另一个有用的类:

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

您可以编写针对 IComparer<T> 的代码,如下所示:
myList.BinarySearch(item, new PropertyComparer<MyClass, int>(c => c.ID));

1

委托技术非常简短(lambda表达式可能更短),因此如果您的目标是更短的代码,则这是一个优势。

然而,实现IComparer(及其泛型等效物)可以使您的代码更具可测试性:您可以向比较类/方法添加一些单元测试。

此外,当组合两个或多个比较器并将它们作为新比较器组合时,您可以重用您的比较器实现。使用匿名委托进行代码重用更难以实现。

因此,总结一下:

匿名委托:代码更短(也许更清晰)

显式实现:可测试性和代码重用。


1
我同意代码重用的观点,但是我并不完全相信可测试性。为什么一个接受 IComparer<T> 的方法比接受 Comparison<T> 更容易测试呢?它们都使用了控制反转。 - Aaronaught
1
@Aaronaught,我觉得我被误解了:两个显式实现都很容易测试(IComparer<T>Comparison<T>),不像匿名委托那样难以测试。 - Ron Klein
啊,我明白了,你指的是对 IComparer<T> 本身进行单元测试,而不是接受它的方法。我无法想象实际上想要对其中一个进行单元测试,但你是对的,如果你想要编写测试,那肯定更容易些。 - Aaronaught

0
在您的情况下,拥有一个 IComparer<T> 而不是 Comparision<T> 委托的优点是,您还可以将其用于 Sort 方法,因此您根本不需要 Comparison 委托版本。
另一个有用的事情是实现一个委托的 IComparer<T> 实现,如下所示:
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));

还有一个更高级的版本:

public class PropertyDelegatorComparer<TSource,TProjected> : DelegatedComparer<TSource>
{
  PropertyDelegatorComparer(Func<TSource,TProjected> projection)
    : base((a,b)=>projection(a).CompareTo(projection(b)))
}

笔误:第一个代码片段第8行缺少闭合的花括号 } - Ron Klein

0

它们确实解决了不同的需求:

IComparable 对于有序对象非常有用。实数应该是可比较的,但复数不行 - 这是不明确的。

IComparer 允许定义可重用、封装良好的比较器。如果比较需要知道一些额外的信息,这将特别有用。例如,您可能想要比较来自不同时区的日期和时间。这可能很复杂,应该使用单独的比较器来完成此任务。

比较方法适用于简单的比较操作,这些操作不够复杂,无需考虑可重用性,例如按客户名字的首字母排序客户列表。这是一个简单的操作,因此不需要额外的数据。同样,这不是对象固有的特性,因为对象在任何方面都没有自然排序。

最后,还有IEquatable,如果您的Equals方法只能确定两个对象是否相等,但如果没有“更大”和“更小”的概念,例如复数或空间中的向量,则可能很重要。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接