Различия между IEquatable <T>, IEqualityComparer <T> и переопределением .Equals() при использовании LINQ в коллекции пользовательских объектов?
У меня возникают трудности с использованием метода Linq.Except() при сравнении двух коллекций настраиваемого объекта.
Я вывел свой класс из Object
и реализовал переопределения для Equals()
, GetHashCode()
и операторов ==
и !=
. Я также создал метод CompareTo()
.
В моих двух коллекциях, в качестве отладочного эксперимента, я взял первый элемент из каждого списка (который является дубликатом) и сравнил их следующим образом:
itemListA[0].Equals(itemListB[0]); // true
itemListA[0] == itemListB[0]; // true
itemListA[0].CompareTo(itemListB[0]); // 0
Во всех трех случаях результат такой, какой я хотел. Однако, когда я использую метод Linq Except(), дублирующие элементы не удаляются:
List<myObject> newList = itemListA.Except(itemListB).ToList();
Узнав о том, как Linq выполняет сравнения, я обнаружил различные (конфликтующие?) методы, которые говорят, что мне нужно наследовать от IEquatable<T>
или IEqualityComparer<T>
и т.д.
Я запутался, потому что, когда я наследую, например, IEquatable<T>
, я должен предоставить новый метод Equals()
другой подписью из того, что я уже переопределил. Должен ли я иметь два таких метода с разными сигнатурами, или мне больше не нужно выводить мой класс из Object
?
Мое определение объекта (упрощенное) выглядит следующим образом:
public class MyObject : Object
{
public string Name {get; set;}
public DateTime LastUpdate {get; set;}
public int CompareTo(MyObject other)
{
// ...
}
public override bool Equals(object obj)
{
// allows some tolerance on LastUpdate
}
public override int GetHashCode()
{
unchecked
{
int hash = 17;
hash = hash * 23 + Name.GetHashCode();
hash = hash * 23 + LastUpdate.GetHashCode();
return hash;
}
}
// Overrides for operators
}
Я заметил, что когда я наследую от IEquatable<T>
, я могу сделать это с помощью IEquatable<MyObject>
или IEquatable<object>
; требования к изменению подписи Equals()
при использовании одного или другого. Каков рекомендуемый способ?
То, что я пытаюсь выполнить:
Я хочу иметь возможность использовать Linq (Distinct/Except), а также стандартные операторы равенства (==
и !=
) без дублирования кода. Сравнение должно позволить двум объектам считаться равными, если их имя идентично, а свойство LastUpdate
находится в пределах нескольких секунд (задано пользователем).
Edit:
Отображение кода GetHashCode()
.
Ответы
Ответ 1
Неважно, переопределяете ли вы object.Equals
и object.GetHashCode
, реализуете IEquatable
или предоставляете IEqualityComparer
. Все они могут работать несколько иначе.
1) Переопределение Equals
и GetHashCode
от object
:
Это базовый случай, в некотором смысле. Как правило, это работает, предполагая, что вы в состоянии отредактировать тип, чтобы гарантировать, что реализация этих двух методов будет по желанию. Нет ничего плохого в том, чтобы делать это во многих случаях.
2) Реализация IEquatable
Ключевым моментом здесь является то, что вы можете (и должны) реализовать IEquatable<YourTypeHere>
. Ключевое различие между этим и # 1 состоит в том, что у вас есть сильная типизация для метода Equals
, а не просто использование object
. Это лучше для удобства программиста (добавленный тип безопасности), а также означает, что любые типы значений не будут помещаться в бокс, поэтому это может повысить производительность для пользовательских структур. Если вы сделаете это, вы должны в значительной степени сделать это в дополнение к # 1, а не вместо. Наличие метода Equals
здесь отличается функциональностью от object.Equals
будет... плохой. Не делайте этого.
3) Реализация IEqualityComparer
Это полностью отличается от первых двух. Идея здесь в том, что объект не получает собственный хэш-код или видит, что он равен чему-то другому. Точка этого подхода состоит в том, что объект не знает, как правильно получить хэш или посмотреть, совпадает ли он с чем-то другим. Возможно, это потому, что вы не контролируете код типа (т.е. Стороннюю библиотеку), и они не удосужились переопределить поведение или, возможно, переопределили его, но вам просто нужно ваше собственное уникальное определение "равенства" в этот конкретный контекст.
В этом случае вы создаете полностью отдельный "сравнительный" объект, который принимает два разных объекта и информирует вас о том, являются они равными или нет или что представляет собой хэш-код одного объекта. При использовании этого решения не имеет значения, что делают методы Equals
или GetHashCode
в самом типе, вы не будете использовать его.
Обратите внимание, что все это полностью не связано с оператором ==
, который является его собственным зверьем.
Ответ 2
Основной шаблон, который я использую для равенства в объекте, следующий. Обратите внимание, что только 2 метода имеют реальную логику, специфичную для объекта. Остальное - это только код плиты котла, который питается этими двумя способами.
class MyObject : IEquatable<MyObject> {
public bool Equals(MyObject other) {
if (Object.ReferenceEquals(other, null)) {
return false;
}
// Actual equality logic here
}
public override int GetHashCode() {
// Actual Hashcode logic here
}
public override bool Equals(Object obj) {
return Equals(obj as MyObject);
}
public static bool operator==(MyObject left, MyObject right) {
if (Object.ReferenceEquals(left, null)) {
return Object.ReferenceEquals(right, null);
}
return left.Equals(right);
}
public static bool operator!=(MyObject left, MyObject right) {
return !(left == right);
}
}
Если вы следуете этому шаблону, нет необходимости предоставлять пользовательский IEqualityComparer<MyObject>
. EqualityComparer<MyObject>.Default
будет достаточно, поскольку он будет полагаться на IEquatable<MyObject>
для выполнения проверок равенства
Ответ 3
Вы не можете "разрешить некоторый допуск на LastUpdate
", а затем использовать реализацию GetHashCode()
, которая использует строгое значение LastUpdate
!
Предположим, что экземпляр this
имеет LastUpdate
в 23:13:13.933
, а экземпляр obj
имеет 23:13:13.932
. Тогда эти два могут сравниться с вашей идеей толерантности. Но если это так, их хэш-коды должны быть одинаковыми. Но это не произойдет, если вам не очень повезло, поскольку DateTime.GetHashCode()
не должен давать один и тот же хеш для этих двух раз.
Кроме того, ваш метод Equals
может быть математически переходным. И "приблизительно равный" нельзя сделать переходным. Его транзитивное замыкание - это тривиальное отношение, которое идентифицирует все.