Как сделать метод Contains корректным для сравнения объектов с одинаковыми значениями
Что проверяют
Понимаете ли вы разницу между ссылочным и значимым равенством и как коллекции решают, что два объекта «одинаковые». Следом обычно спрашивают, что изменится для HashSet<T> или ключа словаря и зачем тогда GetHashCode.
Как отвечать
List<T>.Contains сравнивает элементы через EqualityComparer<T>.Default. Для класса без переопределений это сравнение ссылок, поэтому новый объект с теми же полями не найдётся. Способы исправить:
record: с C# 9record classполучает сравнение по значениям всех свойств, согласованныйGetHashCodeи операторы==и!=автоматически. Для DTO и объектов-значений это самый короткий путь;IEquatable<T>: реализуйте типизированныйEquals(T other), переопределитеEquals(object)иGetHashCode.EqualityComparer<T>.Defaultсначала ищет именноIEquatable<T>, для структур это ещё и избавляет от упаковки;HashCode.Combine: собирает хэш из тех же полей, что участвуют вEquals;- внешний компаратор: если менять класс нельзя или сравнение в разных местах разное, реализуйте
IEqualityComparer<T>и передайте его вEnumerable.Contains(list, item, comparer), в конструкторHashSet<T>илиDictionary<TKey, TValue>; - поиск по ключу: иногда честнее написать
list.Any(x => x.Id == id)и не менять семантику равенства для всего типа.
Отдельно скажите про сложность: List<T>.Contains проходит список целиком, это O(n). Если проверок много, нужен HashSet<T> с корректным GetHashCode, там поиск в среднем O(1).
Где спотыкаются
Переопределяют Equals без GetHashCode. List будет работать, а HashSet и Dictionary начнут терять объекты, и компилятор об этом только предупредит.
Строят равенство на изменяемых свойствах с set. Объект положили в HashSet, поменяли поле, хэш стал другим, и Contains его больше не находит, хотя он внутри.
Ждут, что переопределённый Equals поменяет поведение == у класса. Для обычного класса == остаётся сравнением ссылок, пока вы не перегрузите оператор; у record это сделано за вас.