【问题标题】:Should an override of Equals on a reference type always mean value equality?在引用类型上覆盖 Equals 是否应该总是意味着值相等?
【发布时间】:2013-07-10 14:44:05
【问题描述】:

如果不对引用类型做任何特殊处理,Equals() 将意味着引用相等(即相同的对象)。如果我选择为引用类型覆盖Equals(),是否总是意味着两个对象的值是等价的?

考虑这个可变的Person 类:

class Person
{
    readonly int Id;

    string FirstName { get; set; }
    string LastName { get; set; }
    string Address { get; set; }
    // ...
}

代表完全相同的人的两个对象将始终具有相同的Id,但其他字段可能会随着时间的推移而不同(即在地址更改之前/之后)。

对于这个对象,Equals 可以定义为不同的东西:

  • 值相等:所有字段都相等(代表同一个人但地址不同的两个对象将返回 false)
  • 身份平等:Ids 相等(代表同一个人但地址不同的两个对象将返回 true)
  • 引用相等:即不实现 Equals。

问题:哪些(如果有)更适合这个课程? (或者问题应该是,“这个类的大多数客户期望 Equals() 表现如何?”)

注意事项:

  • 使用值相等使得在HashsetDictionary 中使用此类变得更加困难
  • 使用 Identity Equality 使 Equals 和 = 运算符之间的关系变得奇怪(即在检查两个 Person 对象(p1 和 p2)后,Equals() 返回 true,您可能仍想更新您的引用以指向“较新”的 Person 对象,因为它不是等价的)。例如,下面的代码读起来很奇怪——看起来它什么都不做,但实际上是删除 p1 并添加 p2:

    HashSet<Person> people = new HashSet<Person>();
    people.Add(p1);
    // ... p2 is an new object that has the same Id as p1 but different Address
    people.Remove(p2);
    people.Add(p2);
    

相关问题:

【问题讨论】:

  • 在等式有多种含义的情况下,为什么不提供IEqualityComparer(T)的一些实现,让消费者选择使用哪个等式呢?
  • @Romoku,看起来很合理,然后我认为这意味着您根本不会实现 .Equals()
  • 我认为 Marc Gravell 的回答解决了这个问题。
  • 如果代表完全相同的人的两个对象总是具有相同的Id,那么我希望Equals 只比较Id 字段。对于 Equals 方法,其他任何东西都没有意义,并且如果您要比较可变字段,则可能会很危险。
  • @JimMischel,“其他任何事情对 Equals 来说都没有意义”,为什么不呢?我给出了Equals 的三种含义(每种含义都可能对客户有用)。

标签: c# .net


【解决方案1】:

是的,为此确定正确的规则很棘手。这里没有单一的“正确”答案,它在很大程度上取决于上下文和偏好就我个人而言,我很少费心去想它,只是默认在大多数常规 POCO 类上引用相等:

  • 在哈希集中使用 Person 作为字典键 / 的情况很少
    • 当您这样做时,您可以提供一个自定义比较器,该比较器遵循您希望它遵循的实际规则
    • 但大多数时候,我会简单地使用 int Id 作为字典中的键(等等)无论如何
  • 使用引用相等意味着x==y 在泛型方法中无论x/yPerson 还是object,或者实际上是T,都会给出相同的结果
  • 只要EqualsGetHashCode 兼容,大多数事情都会解决,而做到这一点的一种简单方法是不要覆盖它们

但是,请注意,对于值类型,我总是建议相反,即 明确 覆盖 Equals / GetHashCode;但是,写struct真的不常见

【讨论】:

    【解决方案2】:

    您可以提供多个IEqualityComparer(T) 实现并让消费者决定。

    例子:

    // Leave the class Equals as reference equality
    class Person
    {
        readonly int Id;
    
        string FirstName { get; set; }
        string LastName { get; set; }
        string Address { get; set; }
        // ...
    }
    
    class PersonIdentityEqualityComparer : IEqualityComparer<Person>
    {
        public bool Equals(Person p1, Person p2)
        {
            if(p1 == null || p2 == null) return false;
    
            return p1.Id == p2.Id;
        }
    
        public int GetHashCode(Person p)
        {
            return p.Id.GetHashCode();
        }
    }
    
    class PersonValueEqualityComparer : IEqualityComparer<Person>
    {
        public bool Equals(Person p1, Person p2)
        {
            if(p1 == null || p2 == null) return false;
    
            return p1.Id == p2.Id &&
                   p1.FirstName == p2.FirstName; // etc
        }
    
        public int GetHashCode(Person p)
        {
            int hash = 17;
    
            hash = hash * 23 + p.Id.GetHashCode();
            hash = hash * 23 + p.FirstName.GetHashCode();
            // etc
    
            return hash;
        }
    }
    

    另见:What is the best algorithm for an overridden System.Object.GetHashCode?

    用法:

    var personIdentityComparer = new PersonIdentityEqualityComparer();
    var personValueComparer = new PersonValueEqualityComparer();
    
    var joseph = new Person { Id = 1, FirstName = "Joseph" }
    
    var persons = new List<Person>
    {
       new Person { Id = 1, FirstName = "Joe" },
       new Person { Id = 2, FirstName = "Mary" },
       joseph
    };
    
    var personsIdentity = new HashSet<Person>(persons, personIdentityComparer);
    var personsValue = new HashSet<Person>(persons, personValueComparer);
    
    var containsJoseph = personsIdentity.Contains(joseph);
    Console.WriteLine(containsJoseph); // false;
    
    containsJoseph = personsValue.Contains(joseph);
    Console.WriteLine(containsJoseph); // true;
    

    【讨论】:

      【解决方案3】:

      从根本上说,如果类类型字段(或变量、数组槽等)XY 各自持有对类对象的引用,那么 (Object)X.Equals(Y) 可以回答两个逻辑问题:

      1. 如果 `Y` 中的引用被复制到 `X`(意味着引用被复制),类是否有任何理由期望这样的更改会以任何方式影响程序语义(例如,通过影响当前 * 或`X` 或 `Y` 的任何成员的未来*行为)
      2. 如果 *all* 对 `X` 的目标的引用瞬间神奇地指向 `Y` 的目标,*反之亦然*`,该类是否应该期望这样的更改来改变程序行为(例如,通过改变任何成员的行为*除了基于身份的`GetHashCode`*,或者通过导致存储位置引用不兼容类型的对象)。

      请注意,如果 XY 引用不同类型的对象,则这两个函数都不能合法地返回 true,除非两个类都知道不能有任何存储位置持有对一个对象的引用,而该引用又不能持有对另一个[例如因为这两种类型都是从公共基础派生的私有类,并且它们都不会存储在任何类型不能同时包含对两者的引用的存储位置(this 除外)。

      默认Object.Equals方法回答第一个问题; ValueType.Equals 回答第二个。第一个问题通常适合询问可观察状态可能发生变化的对象实例;第二个适合询问对象instances,其可观察状态不会发生变化即使它们的类型允许。如果XY 各自持有对不同int[1] 的引用,并且两个数组在它们的第一个元素中都持有23,则第一个相等关系应该将它们定义为不同的[将X 复制到Y 将改变X[0] 的行为如果 Y[0] 被修改],但第二个应该认为它们是等效的(交换对 XY 目标的所有引用不会影响任何事情)。请注意,如果数组包含不同的值,则第二个测试应将数组视为不同的,因为交换对象意味着 X[0] 现在将报告 Y[0] 曾经报告的值。

      有一个非常严格的约定,可变类型(System.ValueType 及其后代除外)应该覆盖Object.Equals 以实现第一种类型的等价关系;因为System.ValueType 或其后代不可能实现第一个关系,所以它们通常实现第二个。不幸的是,对于第一种关系覆盖Object.Equals() 的对象没有标准约定应该公开一个测试第二种关系的方法,即使可以定义允许在任意类型的任何两个对象之间进行比较的等价关系。第二个关系在标准模式中很有用,其中不可变类 Imm 持有对可变类型 Mut 的私有引用,但不会将该对象暴露给任何可能实际改变它的代码 [制作 实例 不可变]。在这种情况下,Mut 类无法知道一个实例永远不会被写入,但如果有一个标准方法,Imm 的两个实例可以询问Muts如果引用的持有者从未改变过它们,它们是否会等价他们持有引用。。请注意,上面定义的等价关系没有提及突变,也没有提及Imm 必须使用以确保实例不会发生突变的任何特定方法,但其含义在任何情况下都是明确定义的。持有对Mut 的引用的对象应该知道该引用是否封装了身份、可变状态或不可变状态,因此应该能够适当地实现自己的相等关系。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-08-14
        • 2021-03-28
        • 1970-01-01
        • 2018-02-12
        • 2018-11-16
        • 2014-08-15
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多