Comments 58
… а вы точно статью закончили?
if ((object)this == obj) return true;
Почему вы не используете ReferenceEquals
, который намного читаемее?
var other = obj as Person;
Person
будет равен-по-значению любому наследнику Person
, у которого совпадают три нужных вам значения?
Person будет равен-по-значению любому наследнику Person, у которого совпадают три нужных вам значения?
Это как раз тема следующей статьи. Там поговорим об этом подробно.
А эта-то статья нам зачем нужна в таком объеме?
Без шуток, лично вам объективно не нужна — про хеш-коды (включая их особенности) вы знаете, так же как про Equals и проблему оператора as и потомков класса (в разрезе равенства по значению).
И даже возможное недопонимание вопроса приведения к object и использования оператора == вам не помешает — т.к. с помощью Object.ReferenceEquals вы без ошибок и побочных эффектов можете проверять ссылку на null.
А вот сообществу, уверен, нужна — я довольно часто в legacy-проектах наблюдаю ошибки (существующие и вновь добавляемые) в реализациях инфраструктуры, используемой для сравнения и сортировки объектов.
Если хотя бы десяток человек прочтут и заинтересуются, почитают первоисточники, и станут использовать в работе подходы, описанные у Рихтера, в MSDN и прочих best practices, уже хорошо.
Если хотя бы десяток человек прочтут и заинтересуются, почитают первоисточники, и станут использовать в работе подходы, описанные у Рихтера, в MSDN и прочих best practices, уже хорошо.
Для того, чтобы это случилось, нужно (а) объяснять все спорные места и (б) использовать максимально читаемые best practices. Вы же для первого случая говорите "в следующей статье", а второй просто оспариваете.
А бездумное применение "потому что у Рихтера так написано" опасно.
Почему вы не используете ReferenceEquals, который намного читаемее?
Что читаемее, вопрос предпочтений — ни один из способов не считаю хорошим.
Но если мы хотим сравнить на null, то нужно сравнивать именно на null, а не вызывать перегруженный оператор (который, кстати, может быть внезапно перегружен и в будущем).
Возможно, хорошим способом будет pattern matching в C# 7.0: obj is null.
(В своей практике встречал даже такое, когда проверка переменной на null (if (a == null)) приводила к NullReferenceException, т.к. замечательные программисты, реализовав перегруженный оператор, обращались в нем к свойствам операндов для проверки на равенство по значению, не проверив операнды на null.)
Что читаемее, вопрос предпочтений — ни один из способов не считаю хорошим.
В данном случае это как раз весьма очевидно.
(object)this == obj
Здесь два (!) оператора, из которых оба могут быть перегружены, и даже дефолтную семантику второго надо помнить.
Object.ReferenceEquals(this, obj)
Здесь явно написано, что сравнение идет по ссылке, перегрузка невозможна, поведение всегда одно и то же (и правильное).
Так что здесь вопрос читаемости весьма прост.
Но если мы хотим сравнить на null, то нужно сравнивать именно на null, а не вызывать перегруженный оператор
Чего вы тоже не делаете. О чем и речь.
https://msdn.microsoft.com/library/53k8ybth.aspx
For predefined value types, the equality operator (==) returns true if the values of its operands are equal, false otherwise. For reference types other than string, == returns true if its two operands refer to the same object.
The () operator cannot be overloaded.
Ну и до кучи:
public static bool ReferenceEquals (Object objA, Object objB) {
return objA == objB;
}
А если все-таки использовать ReferenceEquals, то с указанием класса: Object.ReferenceEquals(a, b),
т.к. в текущем классе (или одном из предков) ReferenceEquals может быть переопределен (new).
Получается, что приведение к object и использование оператора "==" ("!=") — первично, а метод ReferenceEquals — лишь обертка.
Так что вопросы по-прежнему к дизайну языка/объектной модели, если все так некрасиво.
The () operator cannot be overloaded.
Зато explicit cast вполне себе переопределяем.
Получается, что приведение к object и использование оператора "==" ("!=") — первично, а метод ReferenceEquals — лишь обертка.
Тем не менее, когда написано Object.ReferenceEquals
— намерение явно читаемо. А когда приведение и сравнение — нет (нужно именно что помнить дизайн языка).
Зато explicit cast вполне себе переопределяем.
https://msdn.microsoft.com/library/ms173105.aspx
User-defined conversions: User-defined conversions are performed by special methods that you can define to enable explicit and implicit conversions between custom types that do not have a base class–derived class relationship. For more information, see Conversion Operators.
Соответственно, переопределение приведения к типу object невозможно ни для одного типа (это легко проверить в коде).
Кроме того, explicit cast к object происходит статически, на этапе компиляции.
Поэтому проверка вида "(object)value == null" является первичным способом проверки на null.
То, что такая проверка неочевидна, и для ее понимания и запоминания нужно знать ряд неочевидных технических подробностей — плохой дизайн языка, объектной модели и платформы.
Однако, в пользу варианта Object.ReferenceEquals говорит скорее, не большая очевидность, которая на мой взгляд, сомнительна, а то, что CLS-спецификация не обязывает язык поддерживать операторы.
А потому любой оператор должен иметь соответствующий ему метод (и во всех классах стандартной библиотеки так и есть).
Поэтому если задать вопрос «а как проверить в .NET (не в C# или VB, в целом .NET) ссылки на равенство, или ссылку на null», то тут верный ответ будет Object.ReferenceEquals.
Но если мы пишем на C#, то удобно пользоваться всеми его возможностями — а для C# первичным является приведение ссылок к object и использование оператора равенства (иначе ведь придется отказаться от всех операторов, для которых есть методы-дублеры).
Повторюсь, не считаю оба способа удобными и очевидными, и полагаю, что оптимальнее будет pattern matching «value is null» при условии, что конструкция будет внутри реализована именно через проверку ссылки, а не вызов перегруженного оператора.
Кстати, как именно реализованы уже имеющиеся операторы "??" и "?."?
Соответственно, переопределение приведение к типу object невозможно ни для одного типа. Кроме того, explicit cast к object происходит статически, на этапе компиляции.
Да, но об этом всем надо помнить. В Object.ReferenceEquals
все явно написано.
То, что такая проверка неочевидна, и для ее понимания и запоминания нужно знать ряд неочевидных технических подробностей — плохой дизайн языка, объектной модели и платформы.
Возможно. Именно поэтому Object.ReferenceEquals
— более читаемое решение.
а для C# первичным является приведение ссылок к object и использование оператора равенства
Вот этот тезис ничем (кроме вашего представления о "первичности") не обоснован.
Кстати, как именно реализованы уже имеющиеся операторы "?? и "?."?
Подозреваю, что через ldnull; ceq;
в IL, который всегда сравнивает "по ссылке".
Есть основания подозревать, что сравнение с null переменной, приведенной к object, тоже происходит через ldnull; ceq;
А ReferenceEquals — это в любом случае метод (в данном случае обертка вокруг оператора).
Другое дело, что, возможно (не проверял), один из этих атрибутов:
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[System.Runtime.Versioning.NonVersionable]
может приводить к тому, что содержимое метода инлайнится при вызове?
В любом случае, оба способа корректны, детерминированы, и не имеют побочных эффектов.
Меня бы устроило, если бы программисты проверяли ссылки на null любым из этих способов.
Есть основания подозревать, что сравнение с null переменной, приведенной к object, тоже происходит через ldnull; ceq;
… и что?
А ReferenceEquals — это в любом случае метод
… и что?
содержимое метода инлайнится при вызове?
Конкретно эти атрибуты не влияют на инлайн (по крайней мере, насколько я знаю). Но JIT может применять любые другие эвристики, чтобы понять, надо ли делать инлайн.
Меня бы устроило, если бы программисты проверяли ссылки на null любым из этих способов.
Вы так и не поняли, что я с самого начала говорю не о сравнении с null
, а о сравнении с другим объектом. Первая строчка вашего Equals
. И я говорил не о функциональной корректности (я подозреваю, что IL-код в релизе будет просто идентичным), а о читаемости.
речь, конечно, о сравнении ссылок
в строчке, которая вам так не по душе, первый операнд — не object, приведенный статически к object, а второй объявлен object, и его тип не будет меняться — поэтому там приведения нет
таким же образом происходит и сравнение с null — его нет нужды приводить к object (он по умолчанию object, если не приведен к другому типу)
… и что?
Все то же — вопрос первичности.
Если мы говорим о C#, то при сравнении ссылок первично сравнение object'ов через оператор сравнения, а метод ReferenceEquals — обертка, т.е., вторичен, и насчет инлайнинга мы можем только предполагать.
Если говорим о CLS, то вопрос первичности отпадает, единственный кандидат — ReferenceEquals.
поэтому там приведения нет
Там написано приведение.
Если мы говорим о C#, то при сравнении ссылок первично сравнение object'ов через оператор сравнения, а метод ReferenceEquals — обертка, т.е., вторичен
Это только ваше определение "первичности".
Если говорим о CLS, то вопрос первичности отпадает, единственный кандидат — ReferenceEquals.
Меня вообще не волнует "первичность". Меня волнует читаемость.
поэтому там приведения нет
вы же об этой строчке
if ((object)this == obj) return true;
у первого операнда приведение к object есть, у второго нет, т.к. он объявлен как object
Меня вообще не волнует «первичность». Меня волнует читаемость.Но это ваше предпочтение, что важнее — первичность (производительность) или читаемость, и ваше видение, что более читаемо.
В статье было необходимо показать обязательность сравнения ссылок перед выполнением сравнения по значению.
Если вы заметили, в тексте рядом с примером кода сразу были приведены оба способа, которые мы обсуждаем.
у первого операнда приведение к object есть, у второго нет,
Я вроде ничего и не говорил про второй.
Но это ваше предпочтение, что важнее — первичность (производительность)
"Первичность" не имеет никакого отношения к производительности. Единственный корректный способ говорить о производительности — это измерения.
Но это ваше предпочтение, что важнее — первичность (производительность) или читаемость
Да, и я считаю, что читаемость обычно важнее, особенно в коде, где легко допустить ошибку.
a.FirstName == b.FirstName &&
a.LastName == b.LastName &&
a.BirthDate == b.BirthDate;
(и в статье об этом упомянуто)
поэтому важно понимать, для чего и как правильно их использовать
как их использовать конкретно в .NET — можно почитать и в MSDN, и у Рихтера
можно почитать, как происходит работа с хеш-кодами в Java
если в целом про природу хэш-кодов, хэш-сумм в криптографии, контрольных сумм, и т.д. — полно литературы и источников
a.FirstName == b.LastName, a.LastName == b.FirstName и a.BirthDate == b.BirthDate
GetHashCode() действительно вернет одинаковые значения, это коллизия — следствие природы хеш-кода, но не ошибка.
Главное, на основании равенства хеш-кодов не делать вывод о равенстве объектов по значению.
Равенство хеш-кодов — повод проверить равенство объектов с помощью Equals.
А вот различие хеш-кодов однозначно говорит о неравенстве объектов.
public override int GetHashCode() {
unchecked {
return ((this.FirstName.GetHashCode()*397 ^
this.LastName.GetHashCode())*397 ^
this.BirthDate.GetHashCode();
}
}
Одно из требований к функции получение хеш-кода — скорость.
Возможно, в реальном проекте, в зависимости от типа объекта, стоит разработать функцию хеширования для минимизации возможных коллизий.
Например, пусть объект имеет 3 булевых поля.
Определенно, XOR трех таких полей будет давать коллизии, и нужно продумать функцию хеширования — например, превращать булево поле в бит, для каждого поля делать сдвиг с определенным шагом, и формировать хеш-код с помощью OR.
Но коллизия из-за того, что у одного пользователя переставлены местами имя и фамилия, представляется редкой (хотя для англоязычного мира, может, и достаточно частой).
Однако, целью этой статьи было показать общие принципы сравнения объектов по значению, включая некоторые подводные камни, и сосредоточиться в большей степени на методе Equals.
Думаю, вопрос, как лучше формировать быстрый и с минимальными коллизиями хеш для объектов, в зависимости от набора их полей — очень интересный, и это тема отдельной статьи.
коллизия из-за того, что у одного пользователя переставлены местами имя и фамилия, представляется редкой
Бесспорно, но сам факт, на мой взгляд, упомянуть все-таки стоило (ну и в идеале подобрать соответствующий пример).
правильно ли это?
Это допустимо. Обычно этого стараются избежать (как раз за счет "сдвига" каждого сравниваемого свойства), но природа хэш-функций такова, что коллизии будут неизбежно.
Так что хотя это и нежелательно, это допустимо.
DateTime разве nullable? Насколько я знаю она не может быть null.
Однако, может возникнуть ситуация, когда необходимо считать объекты некоторого класса равными, если они определенным образом совпадают по своему содержимому (тождественны).
А можете реальный пример привести? Кроме случаев типа tuple.
Tuple — это просто частный случай объекта-значения. Скажем, у меня есть некие метаданные, где ключами выступают сложно-составные значения (которые можно представлять кортежами, но представлять записями намного проще), и вот эти ключи-то и надо сравнивать по значению (и при этом сохранять нормальный ОО-доступ к каждой составной части, а иногда и методы поддерживать).
Прочему в этом случае не использовать tuple и не городить свои классы?
Во-первых, потому, что Upn
и Email
читаемее, чем Item1
и Item2
. Во-вторых, потому, что если вам внезапно нужно недефолтное сравнение строк, вам придется не только городить свои классы, но еще и передавать их в каждую операцию.
Лично я считаю, что отличную идею разведения на два лагеря сущностей-значений/записей (которые тождественны, если тождественна их структура/содержимое) и сущностей-объектов (которые имеют identity и для которых объект тождественнен только самому себе) в дотнете не довели до ума. На struct-ы, которые как раз и должны использоваться для «записей», накладывается слишком много технических ограничений. В результате, выбирать между struct и class приходится чисто их технических соображений, и приходится брать class для реализации типов-записей. В результате чего и имеем типы со смешанным поведением, которые как бы объекты, передаются по ссылке, но сравниваются по значению. Из реальных примеров могу привести Data Transfer Object, которые не более чем записи и должны сравниваться по содержимому.
Еще раз — я конкретный пример хочу увидеть. Для string никто не будет писать такой код, он уже написан. Для tuple тоже.
Я уже более 10 лет программирую на .NET и еще ни разу не написал в реальном проекте свою реализацию Equals.
Из реальных примеров могу привести Data Transfer Object, которые не более чем записи и должны сравниваться по содержимому.
Кому должны? В каком случае DTO вообще сравнивать надо?
Еще раз — я конкретный пример хочу увидеть. Для string никто не будет писать такой код, он уже написан. Для tuple тоже.
Это интересный вопрос.
Да, есть, как уже отметили, сущности-записи, которые нужно сравнивать по значению, но какие конкретно могут быть примеры, кроме стандартных типов вроде целых чисел и строк.
Уже вещественные числа (несмотря на то, что они — struct) в силу своей природы, выбиваются из ряда:
x.Equals(x) returns true, except in cases that involve floating-point types.
Реализация сравнения по значению стандартного Uri неоднозначна — сравнивается только часть свойств, и в зависимости от версии FW, набор сравниваемых свойств менялся.
А что, если нам нужно сравнить Uri иным образом? Или поместить в словарь все имеющиеся варианты записи Uri, а GetHashCode/Equals позволят поместить только часть?
В конкретных проектах еще сложнее — в зависимости от предметной области, нужно выбирать способ сравнения сущностей по значению, и вообще решать нужно ли это делать.
Для той же сущности Person есть куча вариантов:
— Сравнивать в Equals все поля. А как именно, кстати, сравнивать? — для строк-имен — с учетом регистра или без, с учетом какой культуры, учитывать ли ведущие и ведомые пробельные символы, учитывать ликоличество пробельных символов и их внутренние коды внутри имен/фамилий/отчеств, учитывать ли разницу между составными именами с дефисом, если где-то дефис, а где-то тире, и т.д.
— Использовать внутри Equals/GetHashCode только идентификатор типа СНИЛС или SSN. А точно подойдет вариант? — что если нам нужно в словарь поместить все возможные записи, и потом уже разбираться, где дубли с различающимися неключевыми полями и почему?
На мой взгляд, знать механизм сравнения объектов по значению нужно, чтобы понимать, как это работает для типов из стандартной библиотеки (включая нюансы для тех же вещественных чисел, и то, для всех ли типов сравнение реализовано корректно),
и на случай, если вы разрабатываете собственную библиотеку или фреймворк на некую тему — скорее всего, в них будут типы, переменные которых нужно будет сравнивать по значению в клиентском коде.
Но если вы пишите «клиентский» код, то, возможно, лучше вообще избегать в своих типах реализацию сравнения по значению, вследствие неоднозначности этой задачи с технической и предметной точек зрения.
По возможности, строить архитектуру классов таким образом, чтобы эта задача не возникала.
Вообще, спасибо за вопрос. Об этом стоит поговорить в продолжении.
К объектам в используемой объектной среде всегда добавляется уникальный идентификатор. В C++ это указатель, в C#/Java это ссылка, в объектных БД это OID, и даже когда в реляционной БД вы используете суррогатный ключ — это тоже в каком-то смысле искуственный уникальный идентификатор. Собственно, необходимость в таком идентификаторе и есть ответ на вопрос, чем является тип с точки зрения семантики. Возьмём ваш Person: скорее всего у вас это будет тип-объект, т.к. даже если у разных Person совпадают все данные, то всегда есть вероятность, что это разные люди. Иными словами, вам нужен искуственный идентификатор, чтобы их различать. Если же в рамках вашей задачи вас не интересуют люди без российского паспорта, и вы условились различать всех по номеру паспорта, то вы вполне можете рассматривать Person как тип-значение.
А как именно, кстати, сравнивать? — для строк-имен — с учетом регистра или без, с учетом какой культуры, учитывать ли ведущие и ведомые пробельные символы, ...
Это не нужно рассматривать в рамках задачи сравнения Person. Если ФИО нужно сравнивать иначе, например без учёта регистра, нужно тогда для полей Name использовать не стандартный string, а некий NameString, который, к примеру, будет сравниваться регистронезависимо. При этом он всё равно будет оставаться типом-значением. Вопрос сравнения нельзя рассматривать независимо от типизации.
По возможности, строить архитектуру классов таким образом, чтобы эта задача не возникала.
Не могу согласиться с этим, также как не могу согласиться с тем, что в реляционной БД все таблицы должны иметь целочисленный суррогатный ID.
что если нам нужно в словарь поместить все возможные записи, и потом уже разбираться, где дубли с различающимися неключевыми полями и почему?
И здесь опять следует разобраться, как вы понимаете ваши «записи». Если они могут иметь дубли, но вы таки хотите их различать, вы автоматически наделяете их каким-то отличительным признаком, даже если вы пока не понимаете, каким конкретно. Допустим, вы считываете записи из файла, и их содержимое действительно совпадает. Но вы тем не менее считаете эти записи разными. Как вы их различаете? Ну допустим вы можете говорить о порядковом номере записи. В таком случае именно он является идентификатором и его нужно добавить в ваши дублирующиеся «записи», чтобы перестать считать их дублирующимися. Тогда их можно спокойно добавлять в один список. Либо, вы можете объявить эти «записи» объектами, и тогда такой идентификатор даст вам ваша языковая среда. В случае C# это будет ссылка на объект. Вы также сможете положить несколько ваших дублирующихся по содержимому «записей» в один список.
Позже, когда вы решите задачу с дублирующимися записями, вы скорее всего всё-таки захотите отказаться от номера записи в качестве идентификатора и будете использовать содержимое СНИЛС для сравнения — иначе зачем вообще этот СНИЛС, если он не идентифицирует?
Вообще это больная тема для многих разработчиков и проектов, и касается она почти всех архитектурных слоёв — от пользовательского интерфейса до базы данных.
Кому должны? В каком случае DTO вообще сравнивать надо?
Ну, к примеру, определить наличие изменений в ответе сервера, если нет более удобных механизмов вроде ETag. Хочется же один раз такое сравнение написать, и пользоваться им. Логично, что в этом случае DTO должен сравниваться как запись. У него нет своего identity, это лишь слепок состояния объекта.
Кому должны?
У типа всегда есть семантика. Я думаю вы сильно удивитесь, если некая языковая среда скажет что 5 != 5 только потому, что 5 и 5 это разные «объекты». У вас есть определённые ожидания от поведения числовых литералов. Аналогичные ожидания есть и в моём примере с DTO.
Я уже более 10 лет программирую на .NET и еще ни разу не написал в реальном проекте свою реализацию Equals.
Видимо вы не пользовались NHibernate :). Он Equals любит)
Я уже более 10 лет программирую на .NET и еще ни разу не написал в реальном проекте свою реализацию Equals.
И ни одного equality comparer тоже?
Неточно выразился. Ни разу для классов этого не делал. Только пару раз для структур, но это были особенности связанные с оптимизацией.
Ну то есть вам действительно ни разу не приходилось использовать недефолтный компарер, скажем, для HashSet
или Dictionary
?
насколько я помню — да
Ну а у меня это возникает с регулярностью последние полтора года. И как только ключ состоит больше чем из одной строки, начинается нудятина.
Если бы в .NET были бы одни только классы, то все равно пришлось бы решать проблемы различения сущностей-значений и сущностей-объектов.
Очевидно, что предлагаемый в .NET механизм, когда для этого приходится каждый раз вручную с рядом нюансов реализовывать GetHashCode/Equals и опционально Equals и операторы сравнения, неудобен и имеет большой потенциал ошибок.
Очевидно, что предлагаемый в .NET механизм, когда для этого приходится каждый раз вручную с рядом нюансов реализовывать GetHashCode/Equals и опционально Equals и операторы сравнения, неудобен и имеет большой потенциал ошибок.
Не в .net, а в C#. В том же F# берете record type, и ничего не надо реализовывать вручную. Кстати, в C# их тоже обещали, но, кажется, в 7-ом опять не дадут.
Или, может, F# задействует некие механизмы .NET, а C# — нет
— как это было с фильтрами исключений, когда они были на уровне IL и Execution Engine, VB поддерживал их много лет, а C# стал поддерживать только в 6.0
В .NET вообще нет четкой границы, где кончается платформа, а где начинается язык, и наоборот
Поэтому вряд ли это сделают в C# только на уровне компилятора, скорее внесут что-то в рантайм, а в FCL еще атрибуты внесут, что-то вроде EquatableProperty.
Синтаксический сахар. Так что не вижу причин, почему в C# не сделать точно так же.
О сравнении объектов по значению — 1: Beginning