首页 文章

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

提问于
浏览
12

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()是否合理?"in the real world"所有属性的等价意味着对象本身是相等的(与 ImaginaryNumber 一样)?

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

UPDATE

刚碰到这个in MSDN

通常,当期望将类型的对象添加到某种类型的集合时,或者当它们的主要目的是存储一组字段或属性时,实现值相等 . 您可以根据类型中所有字段和属性的比较来定义值相等,或者可以将定义基于子集 . 但无论是哪种情况,还是在类和结构中,您的实现都应遵循等效的五个保证:

4 回答

  • 13

    我开始意识到我希望Equals意味着两种不同的东西,具体取决于上下文 . 在权衡了这里的输入以及here之后,我已根据我的具体情况确定了以下内容:

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

    那个决定让我仍然错过了 Value 平等的概念(如discussed on MSDN

    定义类或结构时,您可以决定是否为类型创建值相等(或等效)的自定义定义 . 通常,当期望将类型的对象添加到某种类型的集合时,或者当它们的主要目的是存储一组字段或属性时,实现值相等 .

    希望 Value 平等概念(或者我称之为“等价”)的典型案例是单元测试 .

    特定

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

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

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

  • 11

    查看Guidelines and rules for GetHashCode by Eric Lippert .

    规则:GetHashCode返回的整数必须永远不会改变,而对象包含在依赖于哈希代码保持稳定的数据结构中虽然很危险,但允许哈希代码值可以变异的对象是危险的对象的字段变异 .

  • 0

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

    EDIT

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

    HashSet<T> 是一个性能集合,为了实现该性能,它完全依赖于 GetHashCode 在集合的生命周期中保持不变 . 如果您想要这种性能,那么您需要遵循这些规则 . 如果你 can't 然后你将不得不切换到像 List<T> 这样的其他东西

相关问题