Комментарии 13
if (this.GetType() != obj.GetType()) return false;
Следует отметить, что данный способ не является однозначным
Почему же? Он полностью однозначен: .net дает ожидаемое поведение в отношении оператора !=
, примененного к операндам типа Type
(даже если там на самом деле RuntimeType
, как обычно бывает во время выполнения). Если вам удалось добиться от него других результатов — покажите, как.
Однако, из описания методов Equals класса Type.Equals(Object) представляется, что они не предназначены для сравнения непосредственно объектов класса Type.
Но почему? Они как раз для этого и предназначены, просто они учитывают специфику работы с Type
и RuntimeType
.
В частности, у RuntimeType
первый метод как раз перекрыт, и имеет вид obj == (object)this
(второй не перекрыт, потому что мы не знаем, чем является второй операнд).
Исходя из анализа трех документированных способов сравнения объектов класса Type, представляется, что наиболее корректным способом сравнения объектов будет использование операторов "==" и "!=", и, в зависимости от целевой платформы (Target Platform) при сборке, исходный код будет собран либо с использованием сравнения по ссылке (идентично первому варианту), либо с использованием перегруженных операторов "==" и "!=".
В реальной жизни слева и справа у вас будет RuntimeType
чуть реже, чем всегда. Что означает, что будет вызван перегруженный оператор ==
, который, внезапно, выглядит вот так:
public static bool operator ==(RuntimeType left, RuntimeType right) => object.ReferenceEquals(left, right);
Так что в итоге, в реальной жизни, в которой вы имеете дело с "обычным" типом, у вас оба операнда будут иметь тип RuntimeType
, и что бы вы ни делали — left == right
, left.Equals(right)
, Object.ReferenceEquals(left, right)
, у вас все равно будет одно и то же поведение. Что, в общем-то, позволяет нам выбирать по читаемости (где, конечно, ==
выигрывает).
Для метода Type.Equals(Object) проблема несоответствия требованию (как следствие использования оператора as) "x.Equals(y) returns the same value as y.Equals(x)." не возникнет, т.к. класс Type — абстрактный, если только в потомках класса метод не будет перекрыт некорректным образом. Для предотвращения этой потенциальной проблемы, возможно, стоило объявить метод как sealed.
Нет, не стоило. Как можно видеть, RuntimeType
совершенно разумно перекрывает этот метод, выкидывая приличное количество лишних операций.
(del)
Не как следствие. Коммутативность и транзитивность – независимые свойства.
Мне интересно порассуждать: а что если мы моделируем предметную область, и понимаем, что есть где то метод, который принимает Person и внутри вызывает Equals(), например легаси код. Тут мы создаем наследника PersonEx и начинаем теперь экземпляры PersonEx передавать в метод. В легаси коде мы не знаем что у нас может быть PersonEx и что мы теперь добавили сравнение по типу. При дебаге в рантайме мы то конечно увидим, что объекты уже разного типа, но с точки зрения метода мы не сможем отличить PersonEx и Person, если, например у них равны все поля кроме MiddleName. Получается ли нарушение LSP в таком случае?
Легаси код будет принимать объекты типа Person не зная, Person это или PersonEx
В каким порядке при сравнении легаси код будет вызывать Equals — person.Equals(personEx) или personEx.Equals(person) — недетерминировано.
В этом случае, чтобы обеспечить требование "x.Equals(y) == y.Equals(x)" и получить в легаси коде на выходе всегда верный результат сравнения (т.к. для легаси кода x и y — всегда Person), вам нужно в PersonEx.Equals при сравнении проверять/приводить тип входящего дважды — к PersonEx, и, если нет, то к Person.
Тогда вы получаете немасштабируемый код — любое новое наследование от Person или PersonEx потребует копипасты одинаковых проверок во всех типах иерархии, порожденных от Person.
И в любом случае, это будет неверно с предметной точки зрения, т.к. сравнивать записи "Иван Иванов" и "Иван Иванович Иванов" не имеет смысла — вы не сможете сказать, равны эти записи или нет.
Если же вы по каким то причинам используете запись "Иван Иванович Иванов" (PersonEx) именно как "Иван Иванов" (Person), т.е. везде при работе с данной записью не используете отчество, то встает вопрос, почему используется PersonEx, а не Person.
А вот реальный кейс, когда вы можете использовать записи разного типа:
Предположим, вам нужно выбрать всех "Иванов Ивановых" независимо от отчества.
В этом случае нужно использовать внешний компаратор, а не вшивать логику выборки под частные требования в сам тип.
И лучше, если компаратор вообще будет принимать не корневой тип, а интерфейс.
И в любом случае, это будет неверно с предметной точки зрения, т.к. сравнивать записи «Иван Иванов» и «Иван Иванович Иванов» не имеет смысла — вы не сможете сказать, равны эти записи или нет.
ну как раз таки смысл в том, что с точки зрения легаси кода «Иван Иванов» и «Иван Иванович Иванов» для меня одно и то же, так как при проекторовании кода я вообще не знал что может быть Иванович или какой-то PersonEx и мне в принципе не важно это в моем легаси коде. Оба объекта типа Person — OK, оба объекта имеют одинаковые значения в поля Name и Surname — OK, значит они равны. Именно это закладывалось при проектировании старого метода.
Тогда вы получаете немасштабируемый код — любое новое наследование от Person или PersonEx потребует копипасты одинаковых проверок во всех типах иерархии, порожденных от Person.
Согласен. А что если просто при несовпадении типов в PersonEx делегировать сравнение в базовый класс? Тогда конечно получается, что объекты будут равны если их типы не совпадают по базовому сравнению. В этом, ВОЗМОЖНО, есть смысл, так как тогда мы LSP не нарушим и будем всегда уверены, что наш протестированный код работает как надо.
т.е. везде при работе с данной записью не используете отчество, то встает вопрос, почему используется PersonEx, а не Person.
ну вот был у меня написан метод, когда еще не предполагалось наличие PersonEx, и я ожидаю что при добавлении наследника мой код продолжит работать как и раньше (опять тот же LSP)
ну как раз таки смысл в том, что с точки зрения легаси кода «Иван Иванов» и «Иван Иванович Иванов» для меня одно и то же, так как при проекторовании кода я вообще не знал что может быть Иванович или какой-то PersonEx и мне в принципе не важно это в моем легаси коде.
Тогда получается, что для нового кода и новой схемы данных, оперирующими PersonEx и передающих PersonEx в легаси-код, два Иванова Ивана с разными отчествами — два разных человека, а легаси-код выведет в какой-нибудь отчет или файл обмена запись только об одном человеке.
Т.е., с предметной (доменной) точки зрения ну никак не получается.
Поэтому думаю, что реализация Equality в самом типе данных имеет очень малую степень применимости — делать сравнение только для объектов с идентичным рантайм-типом, и только для объектов с семантикой "значение" (и сущность Person тут не подходит, кстати).
Во всех остальных случаях нужен кастомный компаратор в зависимости от задачи, и вопрос в том, что, легаси код должен уметь принимать такой компаратор для сравнения.
И спасибо за вопросы и дискуссию по существу. Этим циклом статей я и хотел предложить задуматься о, казалось бы на первый взгляд, такой тривиальной теме, как Object Equality.
Тогда получается, что для нового кода и новой схемы данных, оперирующими PersonEx и передающих PersonEx в легаси-код, два Иванова Ивана с разными отчествами — два разных человека, а легаси-код выведет в какой-нибудь отчет или файл обмена запись только об одном человеке.
наверное мы не до конца друг-друга понимаем, при проектировании легаси кода никто не закладывался на возможные вариации, и часть системы старой уже оттестирована и в продакшене, поэтому она и представляет предметную область. Если я начинаю допиливать (PersonEx и т.д.) то я скорее всего задумываюсь о каких-то других/новых частях бизнес модели (ну например расширить понятие Person).
Поэтому думаю, что реализация Equality в самом типе данных имеет очень малую степень применимости — делать сравнение только для объектов с идентичным рантайм-типом, и только для объектов с семантикой «значение» (и сущность Person тут не подходит, кстати).
Во всех остальных случаях нужен кастомный компаратор в зависимости от задачи, и вопрос в том, что, легаси код должен уметь принимать такой компаратор для сравнения.
здесь абсолютно согласен, даже можно переформулировать — при проектировании бизнес-логики, имеет смысл внедрять зависимость от компаратора, так как это поможет и в тестировании и в поддержке в будущем. В моем варианте получлось бы так, что когда внесли новый тип данных и использовали старый компаратор — нашли бы ошибку в нем, и принцип LSP не был бы нарушен
ну вот был у меня написан метод, когда еще не предполагалось наличие PersonEx, и я ожидаю что при добавлении наследника мой код продолжит работать как и раньше (опять тот же LSP)
PS. В том то и дело, что одно дело — LSP, и когда я начинал писать примеры для статьи, вначале рассуждал, как вы.
Но потом оказалось, что случае одновременных наследования (с расширением списка полей при сравнении) и реализации Object Equality мы можем обеспечить принцип LSP, но нарушаются другие базовые принципы.
А что если просто при несовпадении типов в PersonEx делегировать сравнение в базовый класс? Тогда конечно получается, что объекты будут равны если их типы не совпадают по базовому сравнению.
Если предметная логика в решаемой задаче это допускает, то, возможно, хорошее решение.
del.
Тогда получается, что для нового кода и новой схемы данных, оперирующими PersonEx и передающих PersonEx в легаси-код, два Иванова Ивана с разными отчествами — два разных человека, а легаси-код выведет в какой-нибудь отчет или файл обмена запись только об одном человеке.
наверное мы не до конца друг-друга понимаем, при проектировании легаси кода никто не закладывался на возможные вариации, и часть системы старой уже оттестирована и в продакшене, поэтому она и представляет предметную область. Если я начинаю допиливать (PersonEx и т.д.) то я скорее всего задумываюсь о каких-то других/новых частях бизнес модели (ну например расширить понятие Person).
Поэтому думаю, что реализация Equality в самом типе данных имеет очень малую степень применимости — делать сравнение только для объектов с идентичным рантайм-типом, и только для объектов с семантикой «значение» (и сущность Person тут не подходит, кстати).
Во всех остальных случаях нужен кастомный компаратор в зависимости от задачи, и вопрос в том, что, легаси код должен уметь принимать такой компаратор для сравнения.
здесь абсолютно согласен, даже можно переформулировать — при проектировании бизнес-логики, имеет смысл внедрять зависимость от компаратора, так как это поможет и в тестировании и в поддержке в будущем. В моем варианте получлось бы так, что когда внесли новый тип данных и использовали старый компаратор — нашли бы ошибку в нем, и принцип LSP не был бы нарушен
О сравнении объектов по значению — 2, или Особенности реализации метода Equals