Я понял, что хотел, чтобы «Равные» означали две разные вещи, в зависимости от контекста. После взвешивания входных данных здесь, а также здесь я остановился на следующем для моей конкретной ситуации:
Я не переопределяю 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));