【问题标题】:When Should a .NET Class Override Equals()? When Should it Not?.NET 类何时应该重写 Equals()?什么时候不应该?
【发布时间】:2012-03-31 07:59:17
【问题描述】:

VS2005 文档Guidelines for Overloading Equals() and Operator == (C# Programming Guide) 部分声明

不建议在非不可变类型中覆盖运算符 ==。

较新的 .NET Framework 4 文档 Guidelines for Implementing Equals and the Equality Operator (==) 省略了该声明,尽管社区内容中的一篇文章重复了该声明并引用了旧文档。

似乎至少对于一些琐碎的可变类来说重写 Equals() 是合理的,例如

public class ImaginaryNumber
{
    public double RealPart { get; set; }
    public double ImaginaryPart { get; set; }
}

在数学中,具有相同实部和相同虚部的两个虚数实际上在测试相等性的时间点相等。断言它们不相等是不正确的,如果具有相同 RealPart 和 ImaginaryPart 的单独对象未被 Equals() 覆盖,则会发生这种情况。

另一方面,如果覆盖 Equals(),则还应该覆盖 GetHashCode()。如果将覆盖 Equals() 和 GetHashCode() 的 ImaginaryNumber 放置在 HashSet 中,并且可变实例更改了它的值,则该对象将不再在 HashSet 中找到。

MSDN 是否不正确地删除了关于不为非不可变类型覆盖 Equals()operator== 的指南?

对于“在现实世界中”所有属性的等价性意味着对象本身是相等的(如ImaginaryNumber)的可变类型重写 Equals() 是否合理?

如果合理,当对象实例参与 HashSet 或其他依赖于 GetHashCode() 不变的东西时,如何最好地处理潜在的可变性?

更新

刚刚遇到这个in MSDN

通常,当类型的对象是 期望被添加到某种集合中,或者当他们的 主要目的是存储一组字段或属性。你可以 将您对价值平等的定义建立在对所有 类型中的字段和属性,或者您可以将定义基于 子集。但在任何一种情况下,在类和结构中,你的 实施应遵循等效的五项保证:

【问题讨论】:

  • 我认为ImaginaryNumber 是可变类型是不合理的。
  • 实际上,您可能应该将 ImaginaryNumber 实现为不可变的值类型(结构)。
  • intlongdouble 等...是可变的。 55 - 您无法更改它的含义。
  • @EricJ。 变量 i 改变了,但Int32 0 仍然是0
  • 为什么 ComplexNumber 应该是不可变的示例:考虑“ComplexNumber a, b, c;”。 Numbers 的期望是 a、b、c 是独立的。 “a = 新的 ComplexNumber(1, 2); b = a;”然后“a.RealPart = -1;”这也将改变 b,除非将 ComplexNumber 定义为结构。或者将 RealPart 设为只读,不允许设置 a.RealPart。然后更改为 a 由“a = new ComplexNumber(-1, a.ImaginaryPart);”完成可以制作一个方便的方法 public ComplexNumber SetReal(double value) { return new ComplexNumber(value, this.ImaginaryPart); }。调用:“a = a.SetReal(-1);”

标签: c# equals gethashcode


【解决方案1】:

我开始意识到我希望 Equals 有两种不同的含义,具体取决于上下文。在权衡了这里的输入以及here 之后,我针对我的特殊情况确定了以下内容:

我没有覆盖Equals()GetHashCode(),而是保留了Equals() 表示类的身份相等,而Equals() 表示结构的值相等这一共同但绝不是普遍存在的约定。这个决定的最大驱动力是散列集合中对象的行为(Dictionary<T,U>HashSet<T>,...),如果我偏离了这个约定。

那个决定让我仍然缺少价值平等的概念(如discussed on MSDN

当你定义一个类或结构时,你决定它是否有意义 为值相等(或等价)创建自定义定义 方式。通常,当 类型应该被添加到某种集合中,或者当 它们的主要目的是存储一组字段或属性。

希望值相等(或者我称之为“等价”)概念的典型案例是在单元测试中。

给定

public class A
{
    int P1 { get; set; }
    int P2 { get; set; }
}

[TestMethod()]
public void ATest()
{
    A expected = new A() {42, 99};
    A actual = SomeMethodThatReturnsAnA();
    Assert.AreEqual(expected, actual);
}

测试将失败,因为Equals() 正在测试引用相等性。

当然可以修改单元测试以单独测试每个属性,但这会将等价概念从类中移到类的测试代码中。

为了将知识封装在类中,并为测试等效性提供一致的框架,我定义了一个我的对象实现的接口

public interface IEquivalence<T>
{
    bool IsEquivalentTo(T other);
}

实现通常遵循这种模式:

public bool IsEquivalentTo(A other)
{
    if (object.ReferenceEquals(this, other)) return true;

    if (other == null) return false;

    bool baseEquivalent = base.IsEquivalentTo((SBase)other);

    return (baseEquivalent && this.P1 == other.P1 && this.P2 == other.P2);
}

当然,如果我有足够多的类和足够的属性,我可以编写一个帮助器,通过反射构建表达式树来实现IsEquivalentTo()

最后,我实现了一个扩展方法,测试两个IEnumerable&lt;T&gt;的等价性:

static public bool IsEquivalentTo<T>
    (this IEnumerable<T> first, IEnumerable<T> second)

如果T 实现IEquivalence&lt;T&gt;,则使用该接口,否则使用Equals() 来比较序列的元素。允许回退到Equals() 让它工作,例如除了我的业务对象之外,还有ObservableCollection&lt;string&gt;

现在,我的单元测试中的断言是

Assert.IsTrue(expected.IsEquivalentTo(actual));

【讨论】:

  • 这比考虑哈希码生成和担心冲突和副作用以及等等等等要懒惰得多。这是一件好事:)
  • 我希望 .NET 已经定义了两组虚拟相等测试(或包含一个参数来指示应该测试哪种类型的相等)。让一个对象通过持有一个对象实例的可变类型引用来封装数据,该对象实例永远不会暴露给任何可能改变它的东西,这是一种非常常见的模式;持有引用的对象应提供在该场景中有意义的相等性测试。
  • @supercat 和 EricJ 我同意。似乎是 .Net 中缺少的功能。
  • @ToolmakerSteve:我花了一段时间才为这两个相等性测试找出一个好的定义,这两个测试仅限于所有对象通用的特征。我最终确定的是X.Identical(Y) 应该意味着用对 Y 的引用替换对 X 的 SOME 引用应该没有语义效果,而 X.EquivState(Y) 意味着同时交换 all引用X 和引用Y,反之亦然,除了可能更改X.IdentityHash()Y.IdentityHash() 的值外,没有语义影响。
  • 我发现对象整齐地分为两类:实体(可变,引用相等)和值(不可变,结构相等,实现 IEquatable)。你需要一个结构相等的可变类型来做什么?这对我来说是一种气味。
【解决方案2】:

关于不为可变类型重载== 的 MSDN 文档是错误的。可变类型实现相等语义绝对没有错。两个项目现在可以相等,即使它们将来会改变。

可变类型和相等性的危险通常在它们被用作哈希表中的键或允许可变成员参与GetHashCode 函数时出现。

【讨论】:

  • 因为 == 实际上代表 C# 中的两个不同的运算符,任何类类型是否真的应该重载它是有争议的,因为它可能并不总是很清楚它是代表引用相等测试还是调用== 过载。我会进一步建议,唯一普遍适用于所有对象的平等概念是等价,而不同的类对象只有在它们没有任何不同的方式时才能等价。虽然某些“可变”类型符合该标准,但可以独立变异的事物则不符合。
  • Re "两个项目现在可以相等,即使它们将来会改变。"问题是,如果您将 == 更改为可变类型,则没有人可以安全地使用该类型的对象作为字典键。这是个大问题。它咬了我,即使是我自己写的类型,所以我应该知道得更好。这是微软严重的设计缺陷。正如 EricJ 和 supercat 在其他答案中讨论的那样,对可变变量唯一安全的答案是不理会 Equals,并使用不同的名称定义您自己的相等函数。烦人,因为现在在比较对象时必须使用不同的函数。
【解决方案3】:

查看Eric LippertGuidelines and rules for GetHashCode

规则:当对象包含在依赖于哈希码保持稳定的数据结构中时,GetHashCode 返回的整数不能改变

虽然很危险,但允许对象的哈希码值随着对象的字段发生变异而发生变异。

【讨论】:

  • 我之前确实读过,对于那些没有读过的人来说,这很值得一读。实际上我认为这条线最好地回答了我的问题:If you have such an object and you put it in a hash table then the code which mutates the object and the code which maintains the hash table are required to have some agreed-upon protocol that ensures that the object is not mutated while it is in the hash table. What that protocol looks like is up to you.
【解决方案4】:

我不明白你对GetHashCodeHashSet 的担忧。 GetHashCode 只返回一个帮助HashSet 在内部存储和查找值的数字。如果对象的哈希码发生变化,该对象不会从HashSet 中删除,它就不会存储在最佳位置。

编辑

感谢@Erik J,我明白了。

HashSet&lt;T&gt; 是一个性能集合,要实现该性能,它完全依赖于GetHashCode 在集合的生命周期内保持不变。如果您想要这种性能,那么您需要遵循这些规则。如果您不能,那么您将不得不切换到其他内容,例如 List&lt;T&gt;

【讨论】:

  • 如果在对象添加到 HashSet 后更改了对象的哈希码,myHashSet.Contains(myMutatedObject) 将返回 false。但是,HashSet 本身仍然包含(现在已变异的)对象。因此,允许更改哈希值会破坏 HashSet 合约(Contains 中断)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-02
  • 2020-07-16
  • 2010-10-10
  • 2010-12-30
相关资源
最近更新 更多