.NET类何时应该重写Equals()?什么时候不应该



VS2005文档中的Overloading Equals()和Operator==(C#编程指南)在部分中指出

不建议在非不可变类型中重写运算符==。

较新的.NET Framework 4文档《实现Equals和Equality运算符的指导原则》(==)省略了该语句,尽管Community Content中的一篇帖子重复了该断言并引用了旧文档。

至少对于一些琐碎的可变类(如),重写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()的东西不改变时,如何最好地处理潜在的可变性?

更新

刚刚在MSDN 中发现

通常,当类型的对象应添加到某种集合中,或者当主要目的是存储一组字段或属性。你可以基于对所有类型中的字段和属性,或者可以基于子集。但在任何一种情况下,以及在类和结构中实施应遵循等效性的五个保证:

我开始意识到,根据上下文的不同,我希望Equals表示两种不同的含义。在权衡了这里和这里的输入之后,我针对我的特定情况确定了以下内容:

我并没有重写Equals()GetHashCode(),而是保留了常见但并非普遍存在的约定,即Equals()意味着类的身份相等,Equals()意味着结构的值相等。如果我偏离了这个约定,这个决定的最大驱动因素是散列集合(Dictionary<T,U>HashSet<T>…)中对象的行为。

这个决定让我仍然错过了价值平等的概念(如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);
}

则测试将失败,因为CCD_。

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

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

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));
MSDN文档中关于不为可变类型重载==的内容是错误的。可变类型实现相等语义绝对没有错。两个项目现在可以是相等的,即使它们将来会改变。

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

查看Eric Lippert的GetHashCode指南和规则。

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

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

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

编辑

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

HashSet<T>是一个性能集合,要实现该性能,它完全依赖于GetHashCode在集合的使用寿命内保持不变。如果你想要这样的表现,那么你需要遵守这些规则。如果您不能,那么您将不得不切换到其他类似List<T>

的东西

最新更新