更倾向于使用EqualityComparer<T>而不是IEqualityComparer<T>

40
从MSDN的IEqualityComparer<T>备注部分:
  1. 我们建议您从EqualityComparer<T>类派生,而不是实现IEqualityComparer<T>接口,因为EqualityComparer<T>类使用IEquatable<T>.Equals方法而不是Object.Equals方法进行相等性测试。...

    • 我不理解引文中的论点,即为什么我们应该更喜欢从EqualityComparer<T>类派生而不是实现IEqualityComparer<T>。它暗示了实现IEqualityComparer<T>接口的对象将使用Object.Equals进行相等性测试,但是当我们不想使用Object.Equals或IEquatable<T>.Equals进行相等性测试时,实现IEqualityComparer<T>的整个重点不就在于此吗?

    • 它还暗示如果我们从EqualityComparer<T>派生,则派生类将使用IEquatable<T>.Equals方法进行相等性测试。同样,如果EqualityComparer<T>.Default已经使用Object.Equals或IEquatable<T>.Equals进行测试,那么从EqualityComparer<T>派生的整个重点不就在于我们不想使用Object.Equals或IEquatable<T>.Equals进行相等性测试吗?

  2. ...这与Dictionary<TKey,TValue>类和其他泛型集合的Contains、IndexOf、LastIndexOf和Remove方法一致。

    • 我假设.NET库中的大多数集合在测试元素的默认相等性(即当用户没有向这些集合提供自定义的IEqualityComparer<T>对象时)时,通过调用IEquatable<T>.Equals或Object.Equals(取决于类型为T的元素是否实现了IEquatable<T>)来使用EqualityComparer<T>.Default进行测试。

    • 为什么这些集合(在测试默认相等性时)不直接调用IEquatable<T>.Equals或Object.Equals,而是通过EqualityComparer<T>.Default类来调用呢?


2
你已经给出了引用,但没有说明它们来自哪里,这使得更难理解上下文... - Jon Skeet
3
好的,我将为您进行翻译。以下是需要翻译的内容:@Jon: From here: http://msdn.microsoft.com/en-us/library/ms132123.aspx - Reed Copsey
4
对于第一个问题点赞(+1),我记得在文档中看到过,但是当时并没有理解它,后来就忽略了。 - BlueRaja - Danny Pflughoeft
1
抱歉,感谢Reed和BlueRaya提供链接。 - flockofcode
4个回答

27
关于你的第一个问题:
针对 IEqualityComparer<T> 类的备注部分似乎并没有提供一个理由,说明为什么你应该优先使用继承这个抽象类而不是接口。实际上它更像是在解释为什么平等比较器接口首次被引入。它所说的几乎是无用的,基本上只是在描述默认实现在做什么。如果有的话,“推理”部分听起来更像是你的比较器可以怎么做的一些指导,并且与它实际实现无关。
EqualityComparer<T> 类的公共/受保护接口来看,只有一个可取之处,它实现了非泛型 IEqualityComparer 接口。我认为他们的意思是因为EqualityComparer<T> 实际上实现了非泛型 IEqualityComparer 接口,所以建议继承它,这样您的类就可以在需要非泛型比较器的地方使用它。
IComparer<T> 的备注部分中,这个建议听起来更有道理:

我们建议您从 Comparer<T> 类派生,而不是实现 IComparer<T> 接口,因为 Comparer<T> 类提供了 IComparer.Compare 方法的显式接口实现和获取对象默认比较器的Default属性。

我猜想对于 IEqualityComparer<T> 他们本意也想表达类似的话,但是一些观念被搞混了,导致描述不完整。
关于你的第二个问题:
库中的集合的一个主要目标就是尽可能灵活。其中一个方法就是通过提供 IComparer<T>IEqualityComparer<T> 来允许使用自定义方式比较在其中的对象。当没有提供比较器时,获取默认比较器的实例要比直接进行比较容易得多。这些比较器可以包含调用适当比较的逻辑,以便轻松打包。
例如,默认比较器可以确定 T 是否实现了 IEquatable<T> 并在对象上调用 IEquatable<T>.Equals,否则使用 Object.Equals。在这里封装比重复出现在集合代码中要好得多。
此外,如果他们想要回退到直接调用IEquatable<T>.Equals,他们必须在T上添加一个约束条件,以使这个调用变得可能。这样做会使它变得不够灵活,并且会抵消提供比较器的好处。

1
我喜欢您对非泛型接口实现的想法 - 我希望MSDN能够清楚地表达出来。在大多数情况下,我不认为这会有很大的好处,但这是一个很好的观点 :) - Jon Skeet
非常好的帖子,不管你是否回答这个问题,我都会将其标记为答案:我是否正确地假设,当我们不想使用Object.Equals或IEquatable<T>.Equals来测试T的相等性时,通常会从EqualityComparer<T>/EqualityComparer<T>中派生? - flockofcode
1
@flock:已经回答了您的第二个问题。 - Jeff Mercado
抱歉我没有表述得更清楚 - 我之前问的是关于我的第一个问题中的假设(因此,我们通常从EqualityComparer<T>/EqualityComparer<T>中推导出来,当我们不想使用Object.Equals或IEquatable<T>.Equals测试T的相等性时),是否正确。并不是说我后悔让你回答我的第二个问题 ;) 无论如何,感谢大家的帮助。 - flockofcode
2
@flock: 我错了,我以为你在评论中的问题和你在原始问题中的问题是一样的。针对这个问题,我想说当您想要比较不一定实现 IEquatable<T>,但想要指定比较方式时,可以实现 IEqualityComparer<T> 接口。这并不是因为您不想使用 IEquatable<T>.Equals 提供的实现。 - Jeff Mercado
@Jon:我又发现了几个事实,这会更加巩固我的论点。 - Jeff Mercado

4
我不理解第1个建议。对我来说,它似乎很奇怪。
至于第2个建议 - 很多情况下,你会得到一个类型(比如Dictionary),它有一个IEqualityComparer<T>。虽然实现可以存储空值并显式调用Equals本身,但这样做会很麻烦 - 并且还涉及到重要的丑陋问题,以确保它不必要地装箱实现IEquatable<T>的值类型。使用接口和EqualityComparer<T>.Default明显更简单和更一致的方法。

2
我不理解关于1的建议,这对我来说似乎非常奇怪。我不完全确定你不理解的是什么:是MSDN引用的论点还是我的推理为什么MSDN引用不合理?如果是前者,你是否同意我的推理为什么引用不合理? - flockofcode
6
@flockofcode:MSDN。这些参数没有意义 - 我看不出为什么要优先从EqualityComparer<T>派生而不是直接实现接口。 - Jon Skeet
我也可以问一下:“虽然实现可以存储空值…” a) 你所说的存储空值是什么意思? b) “…并且显式调用Equals本身…” 我猜你的意思是实现会显式调用T的IEquatable<>.Equals? - flockofcode
1
@flockofcode:我的意思是实现可以有一个用于比较的字段,并存储一个空值并将其与字段值非空的情况区别对待...然后,它可以转换为IEquatable<T> - Jon Skeet

0
从基类派生类的主要原因是,基类可以提供可重用的代码,这样您就不必自己编写它。
如果您从接口派生比较器,则必须自己创建给您提供默认比较器的代码(当然,只有在需要时才需要,但嘿,每个人都想要免费功能!)。
类EqualityComparer使用工厂设计模式。
在工厂模式中,我们创建对象而不将创建逻辑暴露给客户端,并使用公共接口引用新创建的对象。
好处是所有EqualityComparer的用户只需调用属性default,一切都为他们完成,以创建公开接口IEqualtiyComparer的正确对象。

这样做的好处是,如果您需要将IEqualityComparer作为函数参数,则无需检查类T是否实现了IEqualtiy<T>,因为Dictionary会为您执行此操作。

如果您从EqualtityComparer<T>派生并确保派生类遵循工厂设计模式,那么在几个相等比较器之间切换就很容易。

此外,与任何工厂一样,您只需更改工厂的参数即可让其生成完全不同的相等比较器。

当然,您可以创建一个相等比较器工厂而不从EqualtyComparer<T>派生,但如果您确实派生,则您的工厂可以创建一种额外类型的相等比较器:默认相等比较器,它是使用IEquatable<T>或Object.Equals的比较器。您不必为此编写任何额外的代码,只需派生即可!

是否有用从EqualtyComparer派生还是取决于您是否认为工厂设计模式有用。

举个例子,假设你想要检查两个字典是否相等。可以考虑几个不同程度的相等性:

  1. 如果字典 X 和 Y 是同一个对象,它们就是相等的
  2. X 和 Y 相等,如果它们具有相等的键(使用字典键比较器),并且它们的值是同一个对象
  3. X 和 Y 相等,如果它们具有相等的键(使用字典键比较器),并且它们的值使用 TValue 的默认相等比较器是相等的
  4. X 和 Y 相等,如果它们具有相等的键(使用字典键比较器),并且它们的值使用提供的值相等比较器是相等的。

如果你从EqualityComparer类派生你的字典比较器类,你已经拥有了比较器(1)。如果提供的TValue比较器是从EqualityComparer派生的,则(3)和(4)之间没有实际区别。

因此,让我们来推导一下可以创建这四个比较器的工厂:

class DictionaryComparerFactory<TKey, TValue> : 
    EqualitiyComparer<Dictionary<TKey, TValue>>
{
    // By deriving from EqaulityComparer, you already have comparer (1)
    // via property Default

    // comparer (4):
    // X and Y are equal if equal keys and equal values using provided value comparer
    public static IEqualityComparer<Dictionary<TKey, TValue>>
        CreateContentComparer(IEqualityComparer<TValue> valueComparer)
    {
        return new DictionaryComparer<TKey, TValue>(valueComparer);
    }

    // comparer (3): X and Y equal if equal keys and values default equal
    // use (4) by providing the default TValue comparer
    public static IEqualityComparer<Dictionary<TKey, TValue>>
        CreateDefaultValueComparer(IEqualityComparer<TValue> valueComparer)
    {
        IEqualityComparer<TValue> defaultValueComparer =
            EqualtiyComparer<TValue>.Default;
        return new DictionaryComparer<TKey, TValue>(defaultValuecomparer);
    }

    // comparer (2): X and Y are equal if equal keys and values are same object
    // use reference equal for values
    public IEqualityComparer<TKey, TValue> CreateReferenceValueComparer()
    {
        IEqualityComparer<TValue> referenceValueComparer = ...
        return new DictionaryComparer<TKey, TValue>(referenceValuecomparer);
    }
}

关于比较器(2),您可以使用在stackoverflow中描述的使用ReferenceEquals的IEqualityComparer作为参考值比较器

现在我们只需提供一个比较器的代码,就有了四个不同的相等性比较器!其余部分被重复使用!

如果没有创建默认比较器的工厂,这种重用就不那么容易。

比较器(4)的代码:使用提供的比较器检查TValue的相等性。

// constructor
protected DictionaryComparer(IEqualityComparer<TValue> valueComparer) : base()
{   // if no comparer provided, use the default comparer
    if (Object.ReferenceEquals(valueComparer, null))
        this.valueComparer = EqualityComparer<TValue>.Default;
    else
        this.valueComparer = valueComparer
}

// comparer for TValue initialized in constructor
protected readonly IEqualityComparer<TValue> valueComparer;

public override bool Equals(Dictionary<TKey, TValue> x, Dictionary<TKey, TValue> y)
{
    if (x == null) { return y == null; } 
    if (y == null) return false;
    if (Object.ReferenceEquals(x, y)) return true;
    if (x.GetType() != y.GetType()) return false;

    // now do equality checks according to (4)
    foreach (KeyValuePair<TKey, TValue> xKeyValuePair in x)
    {
        TValue yValue;
        if (y.TryGetValue(xKeyValuePair.Key, out yValue))
        {   // y also has x.Key. Are values equal?
            if (!this.valueComparer.Equals(xKeyValuePair.Value, yValue))
            {   // values are not equal
                return false;
            }
            // else: values equal, continue with next key
        }
        else
        {   // y misses a key that is in x
            return false;
        }
    }

    // if here, all key/values equal
    return true;
}

现在我们可以使用不同的比较器来简单地比较两个字典:
var dictionaryX = ...
var dictionaryY = ...

var valueComparer1 = ...
var valueComparer2 = ...

var equalityComparer1 = DictionaryComparer<...>.Default();
var equalityComparer2 = DictionaryComparer<...>..CreateDefaultValueComparer();
var equalityComparer3 = DictionaryComparer<...>.CreatereferenceValueComparer();
var equalityComparer4 = DictionaryComparer<...>
   .CreateContentComparer(valueCompaerer1);
var equalityComparer5 = DictionaryComparer<...>
   .CreateContentComparer(valueCompaerer2);

因此,派生使得我的相等比较器工厂始终具有适当的默认比较器。这样可以避免我自己编写代码。


1
几年晚了,但想指出最后一个比较器中的 return true 应该是 return x.Count == y.Count 或者更理想的是在循环之前进行计数检查并提前返回。目前来看,y 字典可能具有所有相同的键和值,以及更多,比较器将返回 true。它目前不是自反的,参数的顺序会有所影响(一个版本返回 true,另一��版本返回 false)。我意识到这只是一个例子,与 OP 的问题无关... 但它引起了我的注意,因为这是一个错误。 - pinkfloydx33

0

如果你在MSDN的解释中改变一个单词,即将derive from改为use,这就有了更多的意义。

在MSDN上:EqualityComparer<T>

我们建议您(不派生自)使用EqualityComparer<T>类,而不是实现IEqualityComparer<T>接口,因为EqualityComparer<T>类使用IEquatable<T>.Equals方法进行相等性测试,而不是Object.Equals方法。这与Dictionary类和其他泛型集合的Contains、IndexOf、LastIndexOf和Remove方法一致。

当然,这只适用于T实现了IEquality<T>

请注意,奇怪的是只有ArrayList<T>具有IndexOfLastIndexOf方法,并且没有重载使用IEqualityComparer<T>的任何方法。其他泛型集合有一个构造函数,该构造函数采用IEqualityComparer<T> 在MSDN上:Comparer<T>: 我们建议您(不要派生自)使用Comparer<T>类而不是实现IComparer<T>接口,因为Comparer<T>类提供了IComparer.Compare方法的显式接口实现和获取对象默认比较器的Default属性。
当然,这仅适用于T实现了IComparableIComparable<T> 如果T没有实现所需的接口,则从EqualityComparer<T>Comparer<T>派生是有用的,因为它免费提供非泛型接口的实现。
另一方面,实现 IEqualityComparer<T>IComparer<T> 可以带来性能上的好处,因为它可以跳过对 IEquatable<T>IComparable<T> 的调用。

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