是否应该在引用类型上覆盖Equals始终意味着值相等?

如果没有对引用类型执行任何特殊操作,则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)
  • 参考平等:即不实施等于。

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

笔记:

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

     HashSet people = new HashSet(); 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#差异
  • 应该什么时候.NET类覆盖等于()? 什么时候不应该?
  • 在C#中简化覆盖等于(),GetHashCode()以获得更好的可维护性

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

  • 当你使用像Person这样的字典键/ /在散列集中的情况最少的情况
    • 当你这样做时,你可以提供一个遵循你想要遵循的实际规则的自定义比较器
    • 但大多数时候,我只使用int Id作为字典(等)中的键
  • 使用引用相等意味着x==y给出相同的结果,无论x / yPerson还是object ,或者在generics方法中确实是T
  • 只要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 { 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 { 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 { new Person { Id = 1, FirstName = "Joe" }, new Person { Id = 2, FirstName = "Mary" }, joseph }; var personsIdentity = new HashSet(persons, personIdentityComparer); var personsValue = new HashSet(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’目标的* all *引用瞬间神奇地指向’Y`的目标,*反之亦然*`,如果该类期望这样的改变改变程序行为(例如通过改变程序的行为)除了基于身份的`GetHashCode` *之外的任何成员*,或者通过使存储位置引用不兼容类型的对象)。

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

默认的Object.Equals方法回答第一个问题; ValueType.Equals回答第二个问题。 第一个问题通常是询问可观察状态可能发生变异的对象实例; 第二个适用于询问对象实例即使其类型允许,其可观察状态也不会发生变异。 如果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实例可以询问他们持有的Mut是否会引用如果参考文献的持有人从未改变它们,则相同 。 请注意,上面定义的等价关系不会引用变异,也不会引用Imm必须使用的任何特定方法来确保实例不会被突变,但其含义在任何情况下都是明确定义的。 持有对Mut的引用的对象应该知道该引用是否封装了身份,可变状态或不可变状态,因此应该能够适当地实现它自己的相等关系。