Что происходит при сравнении ссылочных типов с помощью Equals и операторов в C#
Что проверяют
Понимаете ли вы разницу между равенством ссылок и равенством по значению, и знаете ли, что оператор выбирается компилятором по объявленному типу, а Equals вызывается по фактическому. Это вопрос про то, будет ли ваш тип корректно жить в словаре.
Как отвечать
По умолчанию для ссылочного типа и оператор, и Equals сравнивают ссылки. Исключения надо знать наизусть: у string оператор перегружен на сравнение содержимого, у record компилятор генерирует и сравнение по значению, и операторы.
Главная тонкость в том, что == это статическая перегрузка, выбранная на этапе компиляции по типу переменных. Приведите те же строки к object, и сравнение внезапно станет ссылочным.
object a = new string('x', 3);
object b = new string('x', 3);
a == b; // false, сравниваются ссылки
a.Equals(b); // true, виртуальный Equals у stringEquals виртуальный, он уходит в реальный тип объекта, поэтому в обобщённом коде и коллекциях поведение предсказуемо. Коллекции обращаются к EqualityComparer<T>.Default, который предпочитает реализацию IEquatable<T>.
Дальше про дисциплину: переопределяете Equals, обязаны переопределить GetHashCode по тому же набору полей, и поля лучше брать неизменяемые. Проверку на null внутри собственного оператора делать через is null или ReferenceEquals, иначе оператор вызовет сам себя и уйдёт в бесконечную рекурсию.
Где спотыкаются
Переопределяют только Equals. На списках и в тестах всё сходится, а Dictionary и HashSet перестают находить объект, потому что ищут его по старому хэшу и до сравнения дело не доходит.
Сравнивают оператором переменную типа object или обобщённый параметр и ждут сравнения по значению. В обобщённом коде правильный инструмент это EqualityComparer<T>.Default.Equals, а не оператор.
Пишут проверку типа через сопоставление с образцом и ломают симметрию: базовый объект считает себя равным наследнику, а наследник с дополнительными полями уже нет. Надёжнее сверять фактические типы обоих операндов или запечатать тип.