Когда класс .NET должен переопределять Equals ()?Когда это не должно? - PullRequest
12 голосов
/ 14 марта 2012

Документация VS2005 Рекомендации по перегрузке Equals () и Operator == (Руководство по программированию в C #) состояния в части

Переопределение оператора == в неизменяемых типах не рекомендуется.

Более новая документация .NET Framework 4 Рекомендации по реализации равенства и оператора равенства (==) опускает это утверждение, хотя одна публикация в Содержании сообщества повторяет утверждение и ссылается на более старую документацию.

Кажется, что разумно переопределить Equals () хотя бы для некоторых тривиальных изменяемых классов, таких как

public class ImaginaryNumber
{
    public double RealPart { get; set; }
    public double ImaginaryPart { get; set; }
}

В математике два мнимых числа, которые имеют одинаковую действительную часть и одинаковую мнимую часть, фактически равны в момент проверки равенства. Неверно утверждать, что они не равны , что произошло бы, если бы отдельные объекты с одинаковыми RealPart и ImaginaryPart были Equals (), а не переопределены.

С другой стороны, если кто-то переопределяет Equals (), он должен также переопределить GetHashCode (). Если ImaginaryNumber, который переопределяет Equals () и GetHashCode (), помещается в HashSet, и изменяемый экземпляр меняет свое значение, этот объект больше не будет найден в HashSet.

Был ли неверен MSDN, чтобы удалить указание о том, что нельзя переопределять Equals() и operator== для неизменяемых типов?

Разумно ли переопределять Equals () для изменчивых типов, где "в реальном мире" эквивалентность всех свойств означает, что сами объекты равны (как в ImaginaryNumber)?

Если это разумно, как лучше всего справиться с потенциальной изменчивостью, когда экземпляр объекта участвует в HashSet или чем-то еще, что полагается на то, что GetHashCode () не изменяется?

UPDATE

Только что наткнулся на это в MSDN

Как правило, вы реализуете равенство значений, когда объекты типа ожидается, что будет добавлен в коллекцию какого-либо рода, или когда их Основной целью является хранение набора полей или свойств. Вы можете основывать ваше определение равенства стоимости на сравнении всех поля и свойства в типе, или вы можете основывать определение на подмножество. Но в любом случае, и в обоих классах и структурах, ваш Реализация должна следовать пяти гарантиям эквивалентности:

Ответы [ 4 ]

13 голосов
/ 17 марта 2012

Я понял, что хотел, чтобы «Равные» означали две разные вещи, в зависимости от контекста. После взвешивания входных данных здесь, а также здесь я остановился на следующем для моей конкретной ситуации:

Я не переопределяю Equals() и GetHashCode(), а скорее сохраняю общее, но отнюдь не повсеместное соглашение, что Equals() означает равенство идентичности для классов, а Equals() означает равенство значений для структур. Самым большим фактором, влияющим на это решение, является поведение объектов в хешированных коллекциях (Dictionary<T,U>, HashSet<T>, ...), если я отклоняюсь от этого соглашения.

Из-за этого решения я все еще упускал из виду концепцию равенства ценностей (как обсуждалось в MSDN )

Когда вы определяете класс или структуру, вы решаете, имеет ли это смысл создать собственное определение значения равенства (или эквивалентности) для тип. Как правило, вы реализуете равенство значений, когда объекты ожидается, что тип будет добавлен в коллекцию какого-либо рода, или когда их основная цель - хранить набор полей или свойств.

Типичным случаем для определения понятия равенства значений (или, как я его называю, «эквивалентности») являются модульные тесты.

учитывая

public class A
{
    int P1 { get; set; }
    int P2 { get; set; }
}

[TestMethod()]
public void ATest()
{
    A expected = new A() {42, 99};
    A actual = SomeMethodThatReturnsAnA();
    Assert.AreEqual(expected, actual);
}

тест не пройден, поскольку Equals() проверяет равенство ссылок.

Конечно, модульный тест можно модифицировать для индивидуального тестирования каждого свойства, но это переносит понятие эквивалентности из класса в код теста для класса.

Чтобы сохранить эти знания в классе и обеспечить согласованную структуру для проверки эквивалентности, я определил интерфейс, который мои объекты реализуют

public interface IEquivalence<T>
{
    bool IsEquivalentTo(T other);
}

Реализация обычно следует этому шаблону:

public bool IsEquivalentTo(A other)
{
    if (object.ReferenceEquals(this, other)) return true;

    if (other == null) return false;

    bool baseEquivalent = base.IsEquivalentTo((SBase)other);

    return (baseEquivalent && this.P1 == other.P1 && this.P2 == other.P2);
}

Конечно, если бы у меня было достаточно классов с достаточным количеством свойств, я мог бы написать помощника, который строит дерево выражений с помощью отражения для реализации IsEquivalentTo().

Наконец, я реализовал метод расширения, который проверяет эквивалентность двух IEnumerable<T>:

static public bool IsEquivalentTo<T>
    (this IEnumerable<T> first, IEnumerable<T> second)

Если T реализует IEquivalence<T>, то этот интерфейс используется, в противном случае Equals() используется для сравнения элементов последовательности. Разрешение возврата к Equals() позволяет работать, например, с ObservableCollection<string> в дополнение к моим бизнес-объектам.

Теперь утверждение в моем модульном тесте:

Assert.IsTrue(expected.IsEquivalentTo(actual));
11 голосов
/ 14 марта 2012

Документация MSDN о неперегрузке == для изменяемых типов неверна.В изменчивых типах нет ничего плохого в реализации семантики равенства.Два предмета могут быть равны сейчас, даже если они изменятся в будущем.

Опасности вокруг изменчивых типов и равенства обычно проявляются, когда они используются в качестве ключа в хеш-таблице или позволяют изменяемым элементам участвовать в функции GetHashCode.

7 голосов
/ 15 марта 2012

Ознакомьтесь с Руководством и правилами для GetHashCode от Эрика Липперта .

Правило: целое число, возвращаемое GetHashCode, никогда не должно изменяться, пока объект содержится в структуре данных, которая зависит от стабильности хеш-кода

Допустимо, хотя и опасно, создавать объект, значение хэш-кода которого может изменяться по мере изменения полей объекта.

0 голосов
/ 15 марта 2012

Я не понимаю вашу озабоченность по поводу GetHashCode в отношении HashSet.GetHashCode просто возвращает число, которое помогает HashSet внутренне хранить и искать значения.Если хеш-код для объекта изменяет объект, который не удаляется из HashSet, он просто не будет сохранен в наиболее оптимальной позиции.

РЕДАКТИРОВАТЬ

Благодаря @Erik J я вижу в этом смысл.

HashSet<T> - это коллекция производительности, и для достижения этой производительности она полностью полагается на GetHashCode, являющуюся неизменной в течение всего срока службы коллекции.Если вы хотите, чтобы это выступление, то вы должны следовать этим правилам.Если вы не можете , вам придется переключиться на что-то еще, например List<T>

...