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

时间:2013-07-10 14:44:05

标签: c# .net

如果没有对引用类型执行任何特殊操作,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);
    

相关问题:

3 个答案:

答案 0 :(得分:13)

是的,为此决定正确的规则是棘手的。这里没有单一的“正确”答案,而且在很大程度上取决于背景和偏好。就个人而言,我很少考虑这个问题,只是在大多数常规POCO课程中默认引用相等:

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

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

答案 1 :(得分: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;
    }
}

另请参阅: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;

答案 2 :(得分:1)

从根本上说,如果类类型字段(或变量,数组插槽等)XY各自包含对类对象的引用,则有两个逻辑问题{{1}可以回答:

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

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

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

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