什么时候.NET类覆盖等于()?什么时候不应该?

时间:2012-03-14 19:46:31

标签: c# equals gethashcode

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

  

不建议在非不可变类型中覆盖operator ==。

较新的.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==的指南?

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

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

更新

刚遇到这个in MSDN

  

通常,在类型为的对象时实现值相等   期望被添加到某种集合中,或者当它们被添加到集合中时   主要目的是存储一组字段或属性。您可以   在所有的比较基础上建立价值平等的定义   类型中的字段和属性,或者您可以将定义基于a   子集。但无论是哪种情况,还是在课堂和结构中,你的   实施应遵循等效的五个保证:

4 个答案:

答案 0 :(得分:13)

我开始意识到我希望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<T>

的等价性
static public bool IsEquivalentTo<T>
    (this IEnumerable<T> first, IEnumerable<T> second)

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

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

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

答案 1 :(得分:11)

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

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

答案 2 :(得分:7)

Guidelines and rules for GetHashCode查看Eric Lippert

  

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

     

允许(尽管很危险)使一个对象的哈希码值可以随着对象的字段变异而变异。

答案 3 :(得分:0)

我不理解您对GetHashCode关于HashSet的疑虑。 GetHashCode只返回一个帮助HashSet内部存储和查找值的数字。如果对象的哈希码发生更改,则对象不会从HashSet中删除,它将不会存储在最佳位置。

修改

感谢@Erik J,我明白了这一点。

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