Когда класс.NET должен переопределять Equals()? Когда это не должно?
Документация 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 (), не меняющегося?
ОБНОВИТЬ
Просто наткнулся на это в MSDN
Как правило, вы реализуете равенство значений, когда ожидается, что объекты этого типа будут добавлены в коллекцию какого-либо рода, или когда их основная цель заключается в хранении набора полей или свойств. Вы можете основывать свое определение равенства значений на сравнении всех полей и свойств в типе, или вы можете основывать определение на подмножестве. Но в любом случае, и в обоих классах и структурах, ваша реализация должна следовать пяти гарантиям эквивалентности:
4 ответа
Я понял, что хотел, чтобы 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()
проверяет референтное равенство.
Модульный тест, безусловно, можно модифицировать для индивидуального тестирования каждого свойства, но это переносит концепцию эквивалентности из класса в тестовый код для класса.
Чтобы сохранить эти знания в классе и обеспечить согласованную среду для тестирования эквивалентности, я определил интерфейс, который реализуют мои объекты
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));
Документация MSDN о не перегрузке ==
для изменчивых типов это неправильно. В изменчивых типах нет ничего плохого в реализации семантики равенства. Два предмета могут быть равны сейчас, даже если они изменятся в будущем.
Опасности вокруг изменчивых типов и равенства обычно проявляются, когда они используются в качестве ключа в хэш-таблице или позволяют изменяемым элементам участвовать в GetHashCode
функция.
Ознакомьтесь с Руководством и правилами для GetHashCode Eric Lippert.
Правило: целое число, возвращаемое GetHashCode, никогда не должно изменяться, пока объект содержится в структуре данных, которая зависит от стабильности хеш-кода
Разрешается, хотя и опасно, создавать объект, значение хеш-кода которого может изменяться по мере изменения полей объекта.
Я не понимаю твои опасения по поводу GetHashCode
в отношении HashSet
, GetHashCode
просто возвращает число, которое помогает HashSet
внутренне хранить и искать значения. Если хеш-код для объекта изменяется, объект не удаляется из HashSet
просто не будет храниться в наиболее оптимальной позиции.
РЕДАКТИРОВАТЬ
Благодаря @Erik J я вижу смысл.
HashSet<T>
это коллекция производительности и для достижения этой производительности она полностью полагается на GetHashCode
быть постоянным для жизни коллекции. Если вы хотите, чтобы это выступление, то вы должны следовать этим правилам. Если вы не можете, то вам придется переключиться на что-то другое, как List<T>