繁体   English   中英

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

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

如果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)
  • 参考平等:即不实施平等。

问题:本课程中哪一个(如果有)更可取? (或者也许应该是这样的问题,“大多数此类客户希望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); 

相关问题:

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

  • 在哈希集中使用“ Person作为字典键/之类的情况的次数很少
    • 并且当您这样做时,您可以提供一个遵循您希望它遵循的实际规则的自定义比较器
    • 但在大多数情况下, 无论如何 ,我只会使用int Id作为字典(等)中的键
  • 使用引用相等意味着x==y给出相同的结果,无论x / yPerson还是object ,或者实际上是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<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的目标,反之亦然,那么该类是否应该期望这样的改变来改变程序的行为(例如,通过改变X的行为)呢? *除基于身份的`GetHashCode` *以外的任何成员,或使存储位置引用不兼容类型的对象)。

请注意,如果XY引用不同类型的对象,则两个函数都不能合法地返回true,除非两个类都知道没有任何存储位置可以保存对一个对象的引用,也不能保存对另一个对象的引用(例如,因为两种类型是从公共基础派生的私有类,并且都不存储在类型不能同时引用两者的任何存储位置中(除了this )。

默认的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的引用的对象应该知道该引用是封装身份,可变状态还是不可变状态,因此应该能够适当地实现其自身的相等关系。

暂无
暂无

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

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