引用类型上的Equals重写是否应始终意味着值相等

本文关键字:意味着 Equals 重写 是否 引用类型 | 更新日期: 2023-09-27 18:21:45

如果不对引用类型执行任何特殊操作,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()的行为如何?")

注意:

  • 使用值相等会使在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);
    

相关问题:

  • 为什么Microsoft建议跳过对引用类型实现相等运算符
  • C#==和Equals()之间的差异
  • .NET类何时应该重写Equals()?什么时候不应该
  • 简化C#中重写Equals()、GetHashCode()以获得更好的可维护性

引用类型上的Equals重写是否应始终意味着值相等

是的,为此确定正确的规则很棘手。这里没有一个"正确"的答案,这在很大程度上取决于上下文和偏好。就我个人而言,我很少考虑太多,只是默认在大多数常规POCO类中引用平等:

  • 当您在散列集中使用类似Person的东西作为字典键/时,情况的数量是最小的
    • 当您这样做时,您可以提供一个自定义比较器,该比较器遵循您希望它遵循的实际规则
    • 但大多数时候,我都会简单地使用int Id作为字典中的关键字(等等)
  • 使用引用相等意味着无论x/yPerson还是object,或者实际上是通用方法中的Tx==y都会给出相同的结果
  • 只要EqualsGetHashCode兼容,大多数事情都会迎刃而解,而一个简单的方法就是不覆盖它们

然而,请注意,对于值类型,我总是建议相反,即显式覆盖Equals/GetHashCode;但是,编写一个struct实际上是不常见的

您可以提供多个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;
    }
}

另请参阅:被重写的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;

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

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

注意,如果CCD_ 27和CCD_,两个函数都不能合法地返回true,除非两个类都知道不可能有任何存储位置保存对一个不能同时保存对另一个的引用[例如,因为这两种类型都是从公共基派生的私有类,并且都没有存储在任何存储位置(this除外)中,其类型不能保存对这两个类型的引用]。

默认的Object.Equals方法回答第一个问题;ValueType.Equals回答第二个问题。第一个问题通常是对可观察状态可能发生变化的对象实例提出的适当问题;第二种方法适用于询问对象实例,即使其类型允许,其可观察状态也不会发生变化。如果XY每个都持有对不同int[1]的引用,并且两个数组在其第一个元素中都持有23,则第一等式关系应将它们定义为不同[如果Y[0]被修改,则将X复制到Y将改变X[0]的行为],但第二个应该将它们视为等价的(交换对CCD_ 39和CCD_。请注意,如果数组具有不同的值,第二个测试应该将数组视为不同的,因为交换对象意味着X[0]现在将报告Y[0]用来报告的值)。

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