简体   繁体   English

在引用类型上覆盖等于是否总是意味着值相等?

[英]Should an override of Equals on a reference type always mean value equality?

Without doing anything special for a reference type, Equals() would mean reference equality (ie same object). 如果Equals()引用类型做任何特殊的处理, Equals()将意味着引用相等(即同一对象)。 If I choose to override Equals() for a reference type, should it always mean that the values of the two objects are equivalent? 如果选择为引用类型重写Equals() ,是否总是意味着两个对象的值相等?

Consider this mutable Person class: 考虑以下可变的Person类:

class Person
{
    readonly int Id;

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

Two objects that represent the exact same person will always have the same Id , but the other fields might be different over time (ie before/after an address change). 代表完全相同的人的两个对象将始终具有相同的Id ,但是其他字段可能随时间而不同(即,地址更改之前/之后)。

For this object Equals could be defined to mean different things: 对于此对象,可以将Equals定义为不同的含义:

  • Value Equality: all fields are equal (two objects representing the same person but with different addresses would return false) 值相等:所有字段均相等(两个代表同一个人但地址不同的对象将返回false)
  • Identity Equality: the Ids are equal (two objects representing the same person but with different addresses would return true) 身份平等: Ids相等(两个代表同一个人但地址不同的对象将返回true)
  • Reference Equality: ie don't implement Equals. 参考平等:即不实施平等。

Question: Which (if any) of these is preferable for this class? 问题:本课程中哪一个(如果有)更可取? (Or perhaps the question should be, "how would most clients of this class expect Equals() to behave?") (或者也许应该是这样的问题,“大多数此类客户希望Equals()表现如何?”)

Notes: 笔记:

  • Using Value Equality makes it more difficult to use this class in a Hashset or Dictionary 使用值相等会使在HashsetDictionary使用此类变得更加困难
  • Using Identity Equality makes the relationship between Equals and the = operator strange (ie after a check of two Person objects (p1 and p2) returns true for Equals() , you might still want to update your reference to point to the "newer" Person object since it is not value equivalent). 使用Identity Equality使Equals和=运算符之间的关系变得奇怪(即,在检查两个Person对象(p1和p2)对Equals()返回true之后,您可能仍想更新引用以指向“较新的” Person对象,因为它不是等效的值)。 For example, the following code reads strange--seems like it does nothing, but it is actually removing p1 and adding p2: 例如,以下代码读来很奇怪-似乎它什么也没做,但实际上是在删除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); 

Related Questions: 相关问题:

Yes, deciding the right rules for this is tricky. 是的,为此确定正确的规则很棘手。 There is no single "right" answer here, and it will depend a lot on both context and preference Personally, I rarely bother thinking about it much, just defaulting to reference equality on most regular POCO classes: 这里没有单一的“正确”答案,这将在很大程度上取决于上下文和首选项。就我个人而言,我很少考虑这个问题,只是默认在大多数常规POCO类上引用相等性:

  • the number of cases when you use something like Person as a dictionary-key / in a hash-set is minimal 在哈希集中使用“ Person作为字典键/之类的情况的次数很少
    • and when you do, you can provide a custom comparer that follows the actual rules you want it to follow 并且当您这样做时,您可以提供一个遵循您希望它遵循的实际规则的自定义比较器
    • but most of the time, I'd use simply the int Id as the key in a dictionary (etc) anyway 但在大多数情况下, 无论如何 ,我只会使用int Id作为字典(等)中的键
  • using reference equality means that x==y gives the same result whether x / y are Person or object , or indeed T in a generic method 使用引用相等意味着x==y给出相同的结果,无论x / yPerson还是object ,或者实际上是T在通用方法中
  • as long as Equals and GetHashCode are compatible, most things will just about work out, and one easy way to do that is to not override them 只要EqualsGetHashCode兼容,大多数事情就可以解决了,一种简单的方法是不覆盖它们

Note, however, that I would always advise the opposite for value-types, ie explicitly override Equals / GetHashCode ; 但是请注意,对于值类型,我总是建议相反的做法,即显式重写Equals / GetHashCode ; but then, writing a struct is really uncommon 但是,然后写一个struct 真的很罕见

You could provide multiple IEqualityComparer(T) implementations and let the consumer decide. 您可以提供多个IEqualityComparer(T)实现,并让使用者自行决定。

Example: 例:

// 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;
    }
}

See also: What is the best algorithm for an overridden System.Object.GetHashCode? 另请参见: 重写System.Object.GetHashCode的最佳算法是什么?

Usage: 用法:

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;

Fundamentally, if class-type fields (or variables, array slots, etc.) X and Y each hold a reference to a class object, there are two logical questions that (Object)X.Equals(Y) can answer: 从根本上讲,如果类类型字段(或变量,数组插槽等) XY各自持有对类对象的引用,则(Object)X.Equals(Y)可以回答两个逻辑问题:

  1. If the reference in `Y` were copied to `X` (meaning the reference is copied), would the class have any reason to expect such a change to affect program semantics in any way (eg by affecting the present *or future* behavior of any members of `X` or `Y`) 如果将“ Y”中的引用复制到“ X”(意味着引用已复制),则该类是否有任何理由期望这种更改以任何方式影响程序语义(例如,通过影响当前*或将来*的行为) X或Y的任何成员)
  2. If *all* references to the target of `X` were instantaneously magically made to point to the target of `Y`, *and vice versa*`, should the class expect such a change to alter program behavior (eg by altering behavior of any member *other than an identity-based `GetHashCode`*, or by causing a storage location to refer to an object of incompatible type). 如果瞬间就神奇地使对X的目标的所有引用都指向Y的目标,反之亦然,那么该类是否应该期望这样的改变来改变程序的行为(例如,通过改变X的行为)呢? *除基于身份的`GetHashCode` *以外的任何成员,或使存储位置引用不兼容类型的对象)。

Note that if X and Y refer to objects of different types, neither function may legitimately return true unless both classes know that there cannot be any storage locations holding a reference to one which could not also hold a reference to the other [eg because both types are private classes derived from a common base, and neither is ever stored in any storage location (other than this ) whose type can't hold references to both]. 请注意,如果XY引用不同类型的对象,则两个函数都不能合法地返回true,除非两个类都知道没有任何存储位置可以保存对一个对象的引用,也不能保存对另一个对象的引用(例如,因为两种类型是从公共基础派生的私有类,并且都不存储在类型不能同时引用两者的任何存储位置中(除了this )。

The default Object.Equals method answers the first question; 默认的Object.Equals方法回答第一个问题。 ValueType.Equals answers the second. ValueType.Equals回答第二个。 The first question is generally the appropriate one to ask of object instances whose observable state may be mutated; 通常,第一个问题是可观察状态可能发生突变的对象实例的合适问题。 the second is appropriate to ask of object instances whose observable state will not be mutated even if their types would allow it . 第二种方法适用于询问那些对象实例即使其类型允许,其可观察状态也不会发生变化。 If X and Y each hold a reference to a distinct int[1] , and both arrays hold 23 in their first element, the first equality relation should define them as distinct [copying X to Y would alter the behavior of X[0] if Y[0] were modified], but the second should regard them as equivalent (swapping all references to the targets of X and Y wouldn't affect anything). 如果XY各自持有对不同int[1]的引用,并且两个数组在其第一个元素中均持有23,则第一个等式关系应将它们定义为不同[将X复制到Y将改变X[0]的行为,如果Y[0]已修改],但是第二个应该将它们视为等效(交换对XY目标的所有引用不会有任何影响)。 Note that if the arrays held different values, the second test should regard the arrays as distinct, since swapping the objects would mean X[0] would now report the value that Y[0] used to report). 请注意,如果数组持有不同的值,则第二项测试应将数组视为不同的,因为交换对象将意味着X[0]现在将报告Y[0]用于报告的值)。

There's a pretty strong convention that mutable types (other than System.ValueType and its descendants) should override Object.Equals to implement the first type of equivalence relation; 有一个很强的约定,即可变类型( System.ValueType及其子代除外)应覆盖Object.Equals 。等于实现第一种等效关系; since it's impossible for System.ValueType or its descendants to implement the first relation, they generally implement the second. 由于System.ValueType或其子代无法实现第一个关系,因此他们通常实现第二个关系。 Unfortunately, there's no standard convention by which objects which override Object.Equals() for the first kind of relation should expose a method which tests for the second, even though an equivalence relation could be defined which allowed comparison between any two objects of any arbitrary type. 不幸的是,没有标准约定,即使第一个关系的对象都重写Object.Equals() ,该对象也应公开测试第二个关系的方法,即使可以定义一个等价关系,以允许在任意两个对象之间进行比较类型。 The second relation would be useful in the standard pattern wherein an immutable class Imm holds a private reference to a mutable type Mut but doesn't expose that object to any code that could actually mutate it [making the instance immutable]. 第二种关系在标准模式中很有用,在该模式中,不可变类Imm持有对可变类型Mut的私有引用,但不会将该对象暴露给可能对其进行实际修改的任何代码(使实例不可变)。 In such a case, there's no way for class Mut to know that an instance will never be written, but it would be helpful to have a standard means by which two instances of Imm could ask the Mut s to which they hold references whether they would be equivalent if the holders of the references never mutated them . 在这种情况下, Mut类无法知道永远不会写入实例,但是采用一种标准的方法将有助于Imm两个实例向其持有引用的Mut询问是否愿意如果参考文献的持有者从未对其进行突变,则等同。 Note that the equivalence relation defined above makes no reference to mutation, nor to any particular means which Imm must use to ensure that an instance won't be mutated, but its meaning is well-defined in any case. 请注意,上面定义的等价关系既没有引用突变,也没有引用Imm为确保实例不会突变而必须使用的任何特定方法,但是在任何情况下其含义都是明确定义的。 The object which holds a reference to Mut should know whether that reference encapsulates identity, mutable state, or immutable state, and should thus be able to implement its own equality relation suitably. 持有对Mut的引用的对象应该知道该引用是封装身份,可变状态还是不可变状态,因此应该能够适当地实现其自身的相等关系。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM