Когда должен переопределять класс класса .NET()? Когда это не так?

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

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

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

Кажется, что разумно переопределить 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

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

Ответ 1

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

Я не переопределяю 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() проверяет ссылочное равенство.

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

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

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> в дополнение к моим бизнес-объектам.

Теперь утверждение в моем unit test

Assert.IsTrue(expected.IsEquivalentTo(actual));

Ответ 2

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

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

Ответ 3

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

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

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

Ответ 4

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

ИЗМЕНИТЬ

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

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