首页 文章

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

提问于
浏览
11

如果没有对引用类型执行任何特殊操作, Equals() 将表示引用相等(即相同对象) . 如果我选择覆盖 Equals() 作为引用类型,它是否总是意味着两个对象的值是等价的?

考虑一下这个可变的 Person 类:

class Person
{
    readonly int Id;

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

表示完全相同的人的两个对象将始终具有相同的 Id ,但是其他字段可能随时间不同(即,在地址改变之前/之后) .

对于这个对象,Equals可以定义为不同的东西:

  • 值等于:所有字段都相等(代表同一个人但具有不同地址的两个对象将返回false)

  • Identity Equality: Ids 相等(代表同一个人但具有不同地址的两个对象将返回true)

  • 参考平等:即不实施等于 .

Question: 这类中哪些(如果有的话)更适合这个类? (或许问题应该是,"how would most clients of this class expect Equals() to behave?")

Notes:

  • 使用值平等使得在 HashsetDictionary 中使用此类更加困难

  • 使用Identity Equality使得Equals和 = 运算符之间的关系变得奇怪(即检查两个Person对象(p1和p2)后 Equals() 返回true),您可能仍希望更新引用以指向"newer" Person对象,因为它不等 Value ) . 例如,以下代码读取奇怪 - 似乎它什么都不做,但它实际上是删除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:

3 回答

  • 6

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

    • 使用 Person 之类的东西作为字典键/在哈希集中的情况是最小的

    • 当您这样做时,您可以提供一个遵循您希望它遵循的实际规则的自定义比较器

    • 但是大多数时候,我只使用 int Id 作为字典中的键(等)

    • 使用引用相等意味着 x==y 给出相同的结果,无论 x / yPersonobject ,还是 T 在泛型方法中

    • 只要 EqualsGetHashCode 是兼容的,大多数事情都会解决,一个简单的方法就是不要覆盖它们

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

  • 13

    您可以提供多个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;
    
  • 1

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

    • 如果“Y”中的引用被复制到“X”(意味着复制了引用),那么该类是否有理由期望这种更改以任何方式影响程序语义(例如,通过影响当前或将来 “X”或“Y”的任何成员的行为

    • 如果对* X'目标的* all 引用瞬间神奇地指向'Y的目标,*反之亦然*,如果该类期望这样的改变以改变程序行为(例如通过改变行为)任何成员除了基于身份的GetHashCode *之外,或者通过使存储位置引用不兼容类型的对象) .

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

    默认 Object.Equals 方法回答第一个问题; ValueType.Equals 回答第二个问题 . 第一个问题通常是询问可观察对象实例的适当问题国家可能会发生变异;第二个适用于询问对象实例,即使其类型允许,其可观察状态也不会发生变异 . 如果 XY 各保持于不同 int[1] 的引用,两个阵列在其第一元件保持23,第一平等关系应该将它们定义为不同的[复制 XY 将改变的 X[0] 如果 Y[0] 进行了修改的行为],但第二个应该认为它们是等价的(交换对 XY 的目标的所有引用都不会影响任何东西) . 请注意,如果数组保持不同的值,则第二个测试应该将数组视为不同,因为交换对象意味着 X[0] 现在将报告 Y[0] 用于报告的值 .

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

相关问题