Если переопределение равных по ссылочному типу всегда означает равенство значений?

Не делая ничего специального для ссылочного типа, Equals() будет означать ссылочное равенство (т.е. тот же объект). Если я предпочитаю переопределять Equals() для ссылочного типа, всегда ли это означает, что значения двух объектов эквивалентны?

Рассмотрим этот изменяемый класс Person:

class Person
{
    readonly int Id;

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

Два объекта, которые представляют одного и того же человека, всегда будут иметь одинаковый Id, но другие поля могут быть разными во времени (т.е. до/после изменения адреса).

Для этого объекта Equals могут быть определены как разные вещи:

  • Value Equality: все поля равны (два объекта, представляющие одного и того же человека, но с разными адресами будут возвращать false)
  • Равенство идентичности: Ids равны (два объекта, представляющие одно и то же лицо, но с разными адресами, вернут true)
  • Справочное равенство: т.е. не использовать Equals.

Вопрос: Какой (если таковой имеется) из них предпочтительнее для этого класса? (Или, может быть, вопрос должен быть "как большинство клиентов этого класса ожидают, что Equals() будет вести себя?" )

Примечания:

  • Использование Value Equality затрудняет использование этого класса в Hashset или Dictionary
  • Использование равенства идентичности делает связь между операторами Equals и = странной (т.е. после проверки двух объектов Person (p1 и p2) возвращает true для Equals(), вы все равно можете обновить свою ссылку указывать на "новый" объект 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);
    

Вопросы, относящиеся:

Ответ 1

Да, решение правильных правил для этого сложно. Здесь нет единого "правильного" ответа, и это будет сильно зависеть как от контекста, так и от предпочтения. Лично я редко задумываюсь об этом, просто игнорируя стандартное равенство для большинства обычных классов POCO:

  • количество случаев, когда вы используете что-то вроде Person в качестве словаря-ключа/в хэш-наборе, минимально
    • и когда вы это сделаете, вы можете предоставить пользовательский сопоставитель, который следует за действительными правилами, которые вы хотите, чтобы следовать
    • но большую часть времени я бы просто использовал int Id как ключ в словаре (и т.д.) в любом случае
  • с использованием ссылочного равенства означает, что x==y дает тот же результат, если x/y Person или object, или действительно T в общем методе
  • до тех пор, пока Equals и GetHashCode совместимы, большинство вещей будет просто работать, и один простой способ сделать это - не переопределять их

Обратите внимание, однако, что я всегда буду рекомендовать обратное для типов значений, т.е. явно переопределить Equals/GetHashCode; но тогда запись struct действительно необычная

Ответ 2

Вы можете предоставить несколько 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;

Ответ 3

В принципе, если поля типа класса (или переменные, слоты массива и т.д.) X и Y, каждая из них содержит ссылку на объект класса, есть два логических вопроса, на которые (Object)X.Equals(Y) может ответить:

  • Если ссылка в `Y` была скопирована в` X` (что означает, что ссылка скопирована), у класса будет какая-либо причина ожидать, что такое изменение повлияет на семантику программы каким-либо образом (например, воздействуя на текущий * или будущее * поведение любых членов `X` или` Y`)
  • Если * все * ссылки на цель "X" были мгновенно магически сделаны, чтобы указать на цель "Y", * и наоборот *, если класс ожидает, что такое изменение изменит поведение программы (например, изменяя поведение любого члена *, отличного от "GetHashCode" *, основанного на личность, или заставляя место хранения ссылаться на объект несовместимого типа).

Обратите внимание, что если X и Y относятся к объектам разных типов, ни одна из них не может законно возвращать true, если оба класса не знают, что не могут быть места хранения, содержащие ссылку на ссылку, которая также не может содержать ссылку на другой [например, потому что оба типа являются частными классами, полученными из общей базы, и ни один из них не хранится ни в одном месте хранения (кроме this), тип которого не может содержать ссылки на оба].

Метод по умолчанию Object.Equals отвечает на первый вопрос; ValueType.Equals отвечает на второй. Первый вопрос, как правило, является подходящим для запроса экземпляров объектов, наблюдаемое состояние которых может быть мутировано; второй уместен, чтобы спросить экземпляры объектов, чье наблюдаемое состояние не будет мутировано, даже если их типы позволят это. Если X и Y содержат ссылку на отдельный int[1], и оба массива содержат 23 в своем первом элементе, первое равенство равенства должно определять их как различные [копирование X в Y будет изменять поведение X[0], если Y[0] было изменено], но второе должно рассматривать их как эквивалентные (замена всех ссылок на целевые объекты X и Y не повлияет ни на что). Обратите внимание, что если массивы содержат разные значения, второй тест должен рассматривать массивы как разные, так как замена объектов будет означать, что X[0] теперь сообщит значение, которое Y[0] использовало для сообщения).

Там довольно сильное соглашение о том, что изменяемые типы (кроме System.ValueType и его потомков) должны переопределять Object.Equals для реализации отношения типа первого эквивалента; поскольку для System.ValueType или его потомков невозможно реализовать первое отношение, они обычно реализуют второе. К сожалению, нет стандартного соглашения, по которому объекты, переопределяющие Object.Equals() для первого типа отношения, должны выставлять метод, который проверяет второй, хотя может быть определено отношение эквивалентности, которое допускало сравнение между любыми двумя объектами любого произвольного типа. Второе соотношение было бы полезно в стандартном шаблоне, в котором неизменяемый класс Imm содержит личную ссылку на изменяемый тип Mut, но не подвергает этот объект какому-либо коду, который мог бы фактически его мутировать (что делает экземпляр неизменным). В таком случае нет возможности для класса Mut знать, что экземпляр никогда не будет записан, но было бы полезно иметь стандартное средство, с помощью которого два экземпляра Imm могли бы запросить Mut, которому они держите ссылки, будут ли они эквивалентны, если держатели ссылок никогда не мутировали их. Обратите внимание, что отношение эквивалентности, определенное выше, не ссылается на мутацию и на какие-либо особые средства, которые Imm должен использовать, чтобы гарантировать, что экземпляр не будет мутирован, но его значение четко определено в любом случае. Объект, содержащий ссылку на Mut, должен знать, содержит ли эта ссылка инкапсуляцию идентичности, изменяемое состояние или неизменяемое состояние и, следовательно, может соответствующим образом реализовать свои собственные отношения равенства.