12

如果不对引用类型做任何特别的事情,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() 表现如何?”)

笔记:

  • Hashset使用 Value Equality 使得在or中使用此类变得更加困难Dictionary
  • 使用 Identity Equality 使 Equals 和运算符之间的关系变得=奇怪(即在检查两个 Person 对象(p1 和 p2)后返回 true Equals(),您可能仍希望更新您的引用以指向“较新”的 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);
    

相关问题:

4

3 回答 3

13

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

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

但是请注意,对于值类型,我总是建议相反,即显式覆盖Equals/ GetHashCode;但是,写 astruct真的很少

于 2013-07-10T14:50:58.173 回答
6

您可以提供多种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;
于 2013-07-10T14:53:46.240 回答
1

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

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

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

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

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

于 2013-07-10T15:32:30.820 回答