C#: cравнение объектов

Различают две концепции сравнения значений: сравнение эквивалентности и сравнение порядка. Сравнение эквивалентности предназначено для проверки являются ли два экземпляра одинаковыми. Сравнение порядка выясняет, какой из двух экземпляров будет расположен первым в случае расположения их по возрастанию или убыванию. Ни та, ни другая концепция не являются подмножеством другой, они независимы, и экземпляры могут быть равны с позиции порядка, но не равны с позиции эквивалентности.

Сравнение эквивалентности

Эквивалентность значений и ссылочная эквивалентность

Различают два вида эквивалентности:

  • эквивалентность значений — два значения эквивалентны в каком-то смысле
  • ссылочная эквивалентность — две ссылки ссылаются на один и тот же объект

Значимые типы могут использовать только эквивалентность значений, а ссылочные типы по умолчанию используют ссылочную эквивалентность.

Семантика эквивалентности для DateTimeOffset была перегружена. По умолчанию структуры поддерживают специальную разновидность эквивалентности, называемую структурной эквивалентностью, при которой два значения считаются эквивалентными, если эквивалентны все их члены.

Ссылочные типы могут быть настроены для реализации эквивалентности значений:

Способы сравнения эквивалентности

Сравнение эквивалентности может осуществляться типами с использованием трех стандартных средств:

  • операторы == и !=
  • виртуальный метод object.Equals
  • интерфейс IEquatable<T>

Операторы == и != выполняются статически, т.е. на этапе компиляции. Для значимых типов они вычисляют эквивалентность значений, для ссылочных — ссылочную эквивалентность.

Экземплярный метод Equals определен в типе System.Object, и поэтому доступен всем типам. Он разрешается во время выполнения в соответствии с действительным типом объекта:

Для значимых типов он применяет эквивалентность значений, для ссылочных — ссылочную эквивалентность, для структур — структурную эквивалентность, вызывая Equals на каждом их поле. Если метод Equals вызван на объекте равном null, метод выбросит исключение NullReferenceException.

Тип Object содержит также статический метод Equals, принимающий два аргумента:

Статически метод Equals не выбрасывает исключений если один из операндов равен null:

Тип Object также содержит статический метод ReferenceEquals, выполняющий принудительное сравнение ссылочной эквивалентности:

Если сравнивать значимые типы с помощью метода object.Equals они будут упаковываться (приводиться к объектному типу), что может сказаться на производительности. Интерфейс IEquatable<T> позволяет решить данную проблему:

Реализация данного интерфейса обеспечивает тот же результат, что и вызов экземплярного метода Equals, но без выполнения упаковки значимых типов. Большинство базовых типов .NET реализуют интерфейс IEquatable<T>.

Метод Equals и оператор == не всегда дают одинаковый результат при сравнении:

Оператор == типа double перегружен, чтобы реализовать наиболее правильное с математической точки зрения поведение (NaN не равен ничему, в том числе самому себе). Однако метод Equals всегда должен применять рефлексивную эквивалентность, т.е. x.Equals(x) всегда должен возвращать true. На подобном поведении метода основываются коллекции и словари, иначе они не смогут найти ранее сохраненное в них значение.

Различное поведение == и Equals для значимых типов является редкостью, чаще оно проявляется для ссылочных типов, когда разработчики переопределяют Equals для выполнения эквивалентности значений, а оператор == выполняет стандартную ссылочную эквивалентность:

Переопределение сравнения эквивалентности

Иногда требуется переопределить стандартное поведение сравнения эквивалентности, чтобы изменить ее смысл, например, вместо ссылочной эквивалентности выполнять для класса сравнение эквивалентности значений. Для структур к тому же изменение стандартного поведения сравнения эквивалентности может значительно ускорить выполнение сравнения.

Для переопределения семантики эквивалентности нужно:

  • переопределить методы GetHashCode() и Equals()
  • перегрузить операторы == и != (не обязательно)
  • реализовать интерфейс IEquatable<T> (не обязательно)

Переопределение метода GetHashCode

Метод GetHashCode является виртуальным методом типа object. Он используется в основном в двух типах: System.Collections.Hashtable и System.Collections.Generic.Dictionary<TKey,TValue>. Оба типа представляют собой хэш-таблицы — коллекции, в которых каждый элемент имеет ключ, применяемый для сохранения и извлечения значения. В хэш-таблицах используется очень специфическая система эффективного распределения элементов на основе их ключей. Она требует, чтобы каждый ключ имел хэш-код — число типа Int32. Хэш-код не обязан быть уникальным для каждого ключа, он должен быть насколько возможно разнообразным для достижения хорошей производительности хэш-таблицы. В примере выше для достижения этой цели производится умножение поля на произвольное простое число. При наличии в стуктуре нескольких полей, можно использовать следующий подход:

Ссылочные и значимые типы имеют стандартную реализацию метода GetHashCode, поэтому переопределять этот метод как правило не требуется, если только не переопределяется метод Equals.

При переопределении метода GetHashCode нужно учитывать следующий правила:

  • он должен возвращать одинаковое значение на двух объектах, для которых Equals возвращает true (поэтому они переопределяются вместе)
  • он не должен генерировать исключения
  • он должен возвращать одно и то же значение при многократных вызовах на том же самом объекте (если только объект не изменился)
  • для нормальной работы хэш-таблиц метод GetHashCode должен минимизировать вероятность того, что два разных значения получат один и тот же хэш-код

Переопределение метода Equals

При переопределении метода Equals также нужно учитывать ряд правил:

  • объект не может быть эквивалентен null (если только он не относится к типу допускающему null)
  • эквивалентность рефлексивна — объект эквивалентен сам себе
  • эквивалентность симметрична — если a.Equals(b) то и b.Equals(a)
  • эквивалентность транзитивна — если a.Equals(b), а b.Equals(c), то a.Equals(c)
  • операции эквивалентности повторяемы и надежны — они не должны генерировать исключения

Перегрузка операторов == и !=

В дополнение к переопределению метода Equals можно (но не обязательно) перегрузить операторы == и !=. Это почти всегда делается для структур (т.к. иначе указанные операторы не будут корректно работать), а для классов как правило они не перегружаются, продолжая выполнять ссылочную эквивалентность.

Реализация интерфейса IEquatable<T>

Для полноты можно также реализовать интерфейс IEquatable<T>. Его реализация заключается в реализации в типе метода Equals. В итоге тип будет содержать два метода Equals: один — реализующий интерфейс IEquatable<T>, второй — переопределенный. Оба метода должны давать одинаковый результат.

Сравнение порядка

C# предлагает также несколько способов выполнения сравнения порядка:

  • реализация интерфейсов IComparable (IComparable и IComparable<T>)
  • операторы < и >

Операторы < и являются более специализированными и предназначены в основном для числовых типов. Они разрешаются статически и являются крайне эффективными.

Интерфейсы IComparable

Интерфейсы IComparable определены следующим образом:

Оба интерфейса предоставляют одинаковую функциональность. Метод CompareTo (обоих интерфейсов) работает по следующим правилам:

  • если a находится после b, a.CompareTo(b) возвращает положительное число
  • если a и b одинаковые, a.CompareTo(b) возвращает 0
  • если a находится перед b, a.CompareTo(b) возвращает отрицательное число

Большинство базовых типов реализуют оба интерфейса.

При реализации IComparable следует учитывать одно важное правило: эквивалентность может быть более придирчива, чем сравнение порядка, но не наоборот (если это нарушить, алгоритмы сортировки перестанут работать). Для типа переопределяющего Equals и реализующего IComparable, когда Equals возвращает trueCompareTo должен возвращать 0. Но когда Equals возвращает falseCompareTo может вернуть любое значение. Другими словами эквивалентные объекты всегда равны в плане порядка, но не эквивалентные объекты могут располагаться в разном порядке, в т.ч. быть равными по порядку. Например, при сравнении строк символы  и ǖ будут разными согласно Equals, но одинаковыми согласно CompareTo. При реализации IComparable, чтобы не нарушить это правило, достаточно в первой строке метода CompareTo написать:

После этого можно возвращать то, что нравится.

< и >

Многие типы реализуют операторы < и >:

Как правило операторы < и >, если они реализованы, функционально согласованы с интерфейсом IComparable. Также почти всегда если реализуется IComparable, то перегружаются < и >. Обратное при этом не всегда верно: фактически большинство типов .NET, реализующих IComparable, не перегружают < и >. Это отличается от ситуации с эквивалентностью, при которой обычно производится перегрузка операторов == и != вместе с переопределением метода Equals.

Реализация интерфейсов IComparable

Реализация IComparable представлена в следующем примере:

Компараторы

Стандартные способы сравнения эквивалентности и порядка рассмотренные выше — методы Equals и  GetHashCode и интерфейсы IComparable — играют большую роль в реализации словарей и списков. Тип, для которого Equals и  GetHashCode возвращают осмысленные результаты, может использоваться в качестве ключа в Dictionary и Hashtable. Тип, реализующий интерфейсы IComparable и/или IComparable<T> может использоваться в качестве ключа в отсортированном словаре или списке.

Однако стандартных способов сравнения эквивалентности и порядка зачастую недостаточно ни для реализации словарей, ни для выполнения сравнения. В этом случае могут быть полезны компараторы. Компараторы позволяют переключаться на альтернативное поведение при сравнении эквивалентности или порядка, а также использовать словари с типом ключа, не обладающим внутренней возможностью сравнения эквивалентности или порядка.

Компараторы должны реализовывать один из следующих интерфейсов:

  • IEqualityComparer и/или IEqualityComparer<T> — компараторы эквивалентности, выполняют подключаемое сравнение эквивалентности, а также распознаются Hashtable и Dictionary
  • IComparer и/или IComparer<T> — компараторы упорядочения, выполняют подключаемое сравнение порядка, распознаются отсортированными словарями и позволяет выполнять в них специальную логику упорядочения

Компараторы эквивалентности бесполезны в отсортированных словарях, а компараторы упорядочения бесполезны в несортированных словарях и хэш-таблицах. Интерфейс IEqualityComparer имеет стандартную реализацию — класс EqualityComparer. Кроме того существуют интерфейсы IStructuralEquatable и IStructuralComparable, позволяющие выполнять структурные сравнения на классах и массивах.

IEqualityComparer и EqualityComparer

Интерфейсы IEqualityComparer реализуются компараторами эквивалентности. Интерфейсы имеют следующее определение:

Компаратор может реализовывать оба этих интерфейса или один из них либо наследовать абстрактный класс EqualityComparer:

EqualityComparer реализует оба интерфейса, компаратор же должен переопределить два абстрактных метода. Методы Equals и GetHashCode выполняют тот же функционал, что и описанные выше методы object.

Статическое свойство EqualityComparer<T>.Default возвращает универсальный компаратор эквивалентности, который может применяться в качестве альтернативы статическому методу object.Equals:

IComparer и Comparer

Компараторы упорядочения реализуют интерфейсы IComparer:

Как и с компаратором эквивалентности, имеется абстрактный класс Comparer, который можно наследовать вместо реализации интерфейсов:

Пример реализации и использования компаратора упорядочения:

Компаратор также можно передавать в отсортированный словарь при его создании:

StringComparer

StringComparer — это предопределенный компаратор эквивалентности и упорядочения для сравнения строк, позволяющий учитывать при сравнении язык и регистр. Он реализует оба интерфейса IEqualityComparer and IComparer (плюс их обобщенные версии), поэтому может использоваться с любыми словарями и коллекциями (отсортированными и нет).

Поскольку класс является статическим, экземпляры создаются через его статические свойства и методы.

Метод Create возвращает экземпляр компаратора с указанной культурой, а параметр ignoreCase указывает, стоит учитывать регистр или нет.

Свойства CurrentCulture и CurrentCultureIgnoreCase возвращают экземпляр компаратора, учитывающего текущую культуру (в соответствии с настройками компьютера), первый с учетом регистра, второй без.

Свойства InvariantCulture и InvariantCultureIgnoreCase аналогичны предыдущим, но применяют инвариантную культуру (соответствующую американской).

Свойства Ordinal и OrdinalIgnoreCase возвращают компаратор, выполняющий ординальное сравнение (в соответствии с порядком символов в таблице юникод), последний без учета регистра.

Свойство StringComparer.Ordinal отражает стандартное поведение для сравнения эквивалентности, а свойство StringComparer.CurrentCulture — для сравнения порядка.

Примеры:

IStructuralEquatable и IStructuralComparable

Интерфейсы IStructuralEquatable и IStructuralComparable позволяют использовать структурную эквивалентность и структурное сравнение порядка при сравнении не структур: классов, массивов, кортежей. Структурная эквивалентность подразумевает равенство двух структур если все их поля равны. Определяются интерфейсы IStructuralEquatable и IStructuralComparable следующим образом:

Интерфейсы IStructuralEquatable и IStructuralComparable не являются интерфейсами компаратора, они должны реализовываться непосредственно сравниваемыми объектами (или хотя бы одним из них), а точнее сравниваемые объекты должны приводиться к этим интерфейсам. В этом случае объекты будут считаться равными, если все элементы в их составе равны:

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *