Различают две концепции сравнения значений: сравнение эквивалентности и сравнение порядка. Сравнение эквивалентности предназначено для проверки являются ли два экземпляра одинаковыми. Сравнение порядка выясняет, какой из двух экземпляров будет расположен первым в случае расположения их по возрастанию или убыванию. Ни та, ни другая концепция не являются подмножеством другой, они независимы, и экземпляры могут быть равны с позиции порядка, но не равны с позиции эквивалентности.
Сравнение эквивалентности
Эквивалентность значений и ссылочная эквивалентность
Различают два вида эквивалентности:
- эквивалентность значений — два значения эквивалентны в каком-то смысле
- ссылочная эквивалентность — две ссылки ссылаются на один и тот же объект
Значимые типы могут использовать только эквивалентность значений, а ссылочные типы по умолчанию используют ссылочную эквивалентность.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // Эквивалентность значений: int x = 5, y = 5; Console.WriteLine (x == y); // True // Эквивалентность значений для DateTimeOffset var dt1 = new DateTimeOffset (2010, 1, 1, 1, 1, 1, TimeSpan.FromHours(8)); var dt2 = new DateTimeOffset (2010, 1, 1, 2, 1, 1, TimeSpan.FromHours(9)); Console.WriteLine (dt1 == dt2); // True // Ссылочная эквивалентность: class Foo { public int X; } Foo f1 = new Foo { X = 5 }; Foo f2 = new Foo { X = 5 }; Console.WriteLine (f1 == f2); // False // Но: Foo f3 = f1; Console.WriteLine (f1 == f3); // True |
Семантика эквивалентности для DateTimeOffset
была перегружена. По умолчанию структуры поддерживают специальную разновидность эквивалентности, называемую структурной эквивалентностью, при которой два значения считаются эквивалентными, если эквивалентны все их члены.
Ссылочные типы могут быть настроены для реализации эквивалентности значений:
1 2 3 | Uri uri1 = new Uri ("http://www.linqpad.net"); Uri uri2 = new Uri ("http://www.linqpad.net"); Console.WriteLine (uri1 == uri2); // True |
Способы сравнения эквивалентности
Сравнение эквивалентности может осуществляться типами с использованием трех стандартных средств:
- операторы
==
и!=
- виртуальный метод
object.Equals
- интерфейс
IEquatable<T>
Операторы ==
и !=
выполняются статически, т.е. на этапе компиляции. Для значимых типов они вычисляют эквивалентность значений, для ссылочных — ссылочную эквивалентность.
1 2 3 4 5 6 | int x = 5; int y = 5; Console.WriteLine (x == y); // True object x = 5; object y = 5; Console.WriteLine (x == y); // False |
Экземплярный метод Equals
определен в типе System.Object
, и поэтому доступен всем типам. Он разрешается во время выполнения в соответствии с действительным типом объекта:
1 2 3 | object x = 5; object y = 5; Console.WriteLine (x.Equals (y)); // True |
Для значимых типов он применяет эквивалентность значений, для ссылочных — ссылочную эквивалентность, для структур — структурную эквивалентность, вызывая Equals
на каждом их поле. Если метод Equals
вызван на объекте равном null
, метод выбросит исключение NullReferenceException
.
Тип Object
содержит также статический метод Equals
, принимающий два аргумента:
1 | public static bool Equals (object objA, object objB) |
Статически метод Equals
не выбрасывает исключений если один из операндов равен null
:
1 2 3 4 5 6 | object x = 3, y = 3; Console.WriteLine (object.Equals (x, y)); // True x = null; Console.WriteLine (object.Equals (x, y)); // False y = null; Console.WriteLine (object.Equals (x, y)); // True |
Тип Object
также содержит статический метод ReferenceEquals
, выполняющий принудительное сравнение ссылочной эквивалентности:
1 2 3 4 5 6 7 8 9 10 | class Widget { ... } class Test { static void Main() { Widget w1 = new Widget(); Widget w2 = new Widget(); Console.WriteLine (object.ReferenceEquals (w1, w2)); // False } } |
Если сравнивать значимые типы с помощью метода object.Equals
они будут упаковываться (приводиться к объектному типу), что может сказаться на производительности. Интерфейс IEquatable<T>
позволяет решить данную проблему:
1 2 3 4 | public interface IEquatable<T> { bool Equals (T other); } |
Реализация данного интерфейса обеспечивает тот же результат, что и вызов экземплярного метода Equals
, но без выполнения упаковки значимых типов. Большинство базовых типов .NET реализуют интерфейс IEquatable<T>
.
Метод Equals
и оператор ==
не всегда дают одинаковый результат при сравнении:
1 2 3 | double x = double.NaN; Console.WriteLine (x == x); // False Console.WriteLine (x.Equals (x)); // True |
Оператор ==
типа double
перегружен, чтобы реализовать наиболее правильное с математической точки зрения поведение (NaN
не равен ничему, в том числе самому себе). Однако метод Equals
всегда должен применять рефлексивную эквивалентность, т.е. x.Equals(x)
всегда должен возвращать true
. На подобном поведении метода основываются коллекции и словари, иначе они не смогут найти ранее сохраненное в них значение.
Различное поведение ==
и Equals
для значимых типов является редкостью, чаще оно проявляется для ссылочных типов, когда разработчики переопределяют Equals
для выполнения эквивалентности значений, а оператор ==
выполняет стандартную ссылочную эквивалентность:
1 2 3 4 | var sb1 = new StringBuilder ("foo"); var sb2 = new StringBuilder ("foo"); Console.WriteLine (sb1 == sb2); // False (ссылочная эквивалентность) Console.WriteLine (sb1.Equals (sb2)); // True (эквивалентность значений) |
Переопределение сравнения эквивалентности
Иногда требуется переопределить стандартное поведение сравнения эквивалентности, чтобы изменить ее смысл, например, вместо ссылочной эквивалентности выполнять для класса сравнение эквивалентности значений. Для структур к тому же изменение стандартного поведения сравнения эквивалентности может значительно ускорить выполнение сравнения.
Для переопределения семантики эквивалентности нужно:
- переопределить методы
GetHashCode()
иEquals()
- перегрузить операторы
==
и!=
(не обязательно) - реализовать интерфейс
IEquatable<T>
(не обязательно)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | public struct Area : IEquatable <Area> { public readonly int Measure1; public readonly int Measure2; public Area (int m1, int m2) { Measure1 = Math.Min (m1, m2); Measure2 = Math.Max (m1, m2); } public override bool Equals (object other) // Переопределение Equals { if (!(other is Area)) return false; return Equals ((Area) other); // Вызов метода ниже } public bool Equals (Area other) // Реализация IEquatable<Area> { return Measure1 == other.Measure1 && Measure2 == other.Measure2; } public override int GetHashCode () // Переопределение GetHashCode { return Measure2 * 31 + Measure1; // 31 - произвольно простое число } public static bool operator == (Area a1, Area a2) // Перегрузка оператора == { return a1.Equals (a2); } public static bool operator != (Area a1, Area a2) // Перегрузка оператора != { return !a1.Equals (a2); } } |
Переопределение метода GetHashCode
Метод GetHashCode
является виртуальным методом типа object
. Он используется в основном в двух типах: System.Collections.Hashtable
и System.Collections.Generic.Dictionary<TKey,TValue>
. Оба типа представляют собой хэш-таблицы — коллекции, в которых каждый элемент имеет ключ, применяемый для сохранения и извлечения значения. В хэш-таблицах используется очень специфическая система эффективного распределения элементов на основе их ключей. Она требует, чтобы каждый ключ имел хэш-код — число типа Int32
. Хэш-код не обязан быть уникальным для каждого ключа, он должен быть насколько возможно разнообразным для достижения хорошей производительности хэш-таблицы. В примере выше для достижения этой цели производится умножение поля на произвольное простое число. При наличии в стуктуре нескольких полей, можно использовать следующий подход:
1 2 3 4 5 6 | int hash = 17; // 17 произвольное простое число hash = hash * 31 + field1.GetHashCode(); // 31 другое простое число hash = hash * 31 + field2.GetHashCode(); hash = hash * 31 + field3.GetHashCode(); ... return hash; |
Ссылочные и значимые типы имеют стандартную реализацию метода 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
определены следующим образом:
1 2 | public interface IComparable { int CompareTo (object other); } public interface IComparable<in T> { int CompareTo (T other); } |
Оба интерфейса предоставляют одинаковую функциональность. Метод CompareTo
(обоих интерфейсов) работает по следующим правилам:
- если
a
находится послеb
,a.CompareTo(b)
возвращает положительное число - если
a
иb
одинаковые,a.CompareTo(b)
возвращает0
- если
a
находится передb
,a.CompareTo(b)
возвращает отрицательное число
Большинство базовых типов реализуют оба интерфейса.
При реализации IComparable
следует учитывать одно важное правило: эквивалентность может быть более придирчива, чем сравнение порядка, но не наоборот (если это нарушить, алгоритмы сортировки перестанут работать). Для типа переопределяющего Equals
и реализующего IComparable
, когда Equals
возвращает true
, CompareTo
должен возвращать 0
. Но когда Equals
возвращает false
, CompareTo
может вернуть любое значение. Другими словами эквивалентные объекты всегда равны в плане порядка, но не эквивалентные объекты могут располагаться в разном порядке, в т.ч. быть равными по порядку. Например, при сравнении строк символы ṻ
и ǖ
будут разными согласно Equals
, но одинаковыми согласно CompareTo
. При реализации IComparable
, чтобы не нарушить это правило, достаточно в первой строке метода CompareTo
написать:
1 | if (Equals (other)) return 0; |
После этого можно возвращать то, что нравится.
< и >
Многие типы реализуют операторы <
и >
:
1 | bool after2010 = DateTime.Now > new DateTime (2010, 1, 1); |
Как правило операторы <
и >
, если они реализованы, функционально согласованы с интерфейсом IComparable
. Также почти всегда если реализуется IComparable
, то перегружаются <
и >
. Обратное при этом не всегда верно: фактически большинство типов .NET, реализующих IComparable
, не перегружают <
и >
. Это отличается от ситуации с эквивалентностью, при которой обычно производится перегрузка операторов ==
и !=
вместе с переопределением метода Equals
.
Реализация интерфейсов IComparable
Реализация IComparable
представлена в следующем примере:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | public struct Note : IComparable<Note>, IEquatable<Note>, IComparable { int _semitonesFromA; public int SemitonesFromA { get { return _semitonesFromA; } } public Note (int semitonesFromA) { _semitonesFromA = semitonesFromA; } public int CompareTo (Note other) // Обобщенный IComparable<T> { if (Equals (other)) return 0; return _semitonesFromA.CompareTo (other._semitonesFromA); } int IComparable.CompareTo (object other) // Необобщенный IComparable { if (!(other is Note)) throw new InvalidOperationException ("CompareTo: Not a note"); return CompareTo ((Note) other); } public static bool operator < (Note n1, Note n2) { return n1.CompareTo (n2) < 0; } public static bool operator > (Note n1, Note n2) { return n1.CompareTo (n2) > 0; } public bool Equals (Note other) // Реализация интерфейса IEquatable<Note> { return _semitonesFromA == other._semitonesFromA; } public override bool Equals (object other) // Перегрузка { if (!(other is Note)) return false; return Equals ((Note) other); } public override int GetHashCode () { return _semitonesFromA.GetHashCode(); } public static bool operator == (Note n1, Note n2) { return n1.Equals (n2); } public static bool operator != (Note n1, Note n2) { return !(n1 == n2); } } |
Компараторы
Стандартные способы сравнения эквивалентности и порядка рассмотренные выше — методы Equals
и GetHashCode
и интерфейсы IComparable
— играют большую роль в реализации словарей и списков. Тип, для которого Equals
и GetHashCode
возвращают осмысленные результаты, может использоваться в качестве ключа в Dictionary
и Hash
table. Тип, реализующий интерфейсы IComparable
и/или IComparable<T>
может использоваться в качестве ключа в отсортированном словаре или списке.
Однако стандартных способов сравнения эквивалентности и порядка зачастую недостаточно ни для реализации словарей, ни для выполнения сравнения. В этом случае могут быть полезны компараторы. Компараторы позволяют переключаться на альтернативное поведение при сравнении эквивалентности или порядка, а также использовать словари с типом ключа, не обладающим внутренней возможностью сравнения эквивалентности или порядка.
Компараторы должны реализовывать один из следующих интерфейсов:
IEqualityComparer
и/илиIEqualityComparer<T>
— компараторы эквивалентности, выполняют подключаемое сравнение эквивалентности, а также распознаютсяHashtable
иDictionary
IComparer
и/илиIComparer<T>
— компараторы упорядочения, выполняют подключаемое сравнение порядка, распознаются отсортированными словарями и позволяет выполнять в них специальную логику упорядочения
Компараторы эквивалентности бесполезны в отсортированных словарях, а компараторы упорядочения бесполезны в несортированных словарях и хэш-таблицах. Интерфейс IEqualityComparer
имеет стандартную реализацию — класс EqualityComparer
. Кроме того существуют интерфейсы IStructuralEquatable
и IStructuralComparable
, позволяющие выполнять структурные сравнения на классах и массивах.
IEqualityComparer и EqualityComparer
Интерфейсы IEqualityComparer
реализуются компараторами эквивалентности. Интерфейсы имеют следующее определение:
1 2 3 4 5 6 7 8 9 10 | public interface IEqualityComparer<T> { bool Equals (T x, T y); int GetHashCode (T obj); } public interface IEqualityComparer { bool Equals (object x, object y); int GetHashCode (object obj); } |
Компаратор может реализовывать оба этих интерфейса или один из них либо наследовать абстрактный класс EqualityComparer
:
1 2 3 4 5 6 7 8 | public abstract class EqualityComparer<T> : IEqualityComparer, IEqualityComparer<T> { public abstract bool Equals (T x, T y); public abstract int GetHashCode (T obj); bool IEqualityComparer.Equals (object x, object y); int IEqualityComparer.GetHashCode (object obj); public static EqualityComparer<T> Default { get; } } |
EqualityComparer
реализует оба интерфейса, компаратор же должен переопределить два абстрактных метода. Методы Equals
и GetHashCode
выполняют тот же функционал, что и описанные выше методы object
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | // Класс, экземпляры которого будем сравнивать: public class Customer { public string LastName; public string FirstName; public Customer (string last, string first) { LastName = last; FirstName = first; } } // Компаратор: public class LastFirstEqComparer : EqualityComparer <Customer> { public override bool Equals (Customer x, Customer y) { return x.LastName == y.LastName && x.FirstName == y.FirstName; } public override int GetHashCode (Customer obj) { return (obj.LastName + ";" + obj.FirstName).GetHashCode(); } } // Создадим два экземпляра Customer: Customer c1 = new Customer ("Bloggs", "Joe"); Customer c2 = new Customer ("Bloggs", "Joe"); // Поскольку метод object.Equals в классе Customer не переопределен, // применяется обычная семантика эквивалентности ссылочных типов: Console.WriteLine (c1 == c2); // False Console.WriteLine (c1.Equals (c2)); // False // Та же стандартная семантика применяется в Dictionary // без указания компаратора: var d = new Dictionary<Customer, string>(); d [c1] = "Joe"; Console.WriteLine (d.ContainsKey (c2)); // False // Но с указанием компаратора поведение меняется: var eqComparer = new LastFirstEqComparer(); var d = new Dictionary<Customer, string> (eqComparer); d [c1] = "Joe"; Console.WriteLine (d.ContainsKey (c2)); // True |
Статическое свойство EqualityComparer<T>.Default
возвращает универсальный компаратор эквивалентности, который может применяться в качестве альтернативы статическому методу object.Equals
:
1 2 3 4 | static bool Foo<T> (T x, T y) { bool same = EqualityComparer<T>.Default.Equals (x, y); .... |
IComparer и Comparer
Компараторы упорядочения реализуют интерфейсы IComparer
:
1 2 3 4 5 6 7 8 | public interface IComparer { int Compare(object x, object y); } public interface IComparer <in T> { int Compare(T x, T y); } |
Как и с компаратором эквивалентности, имеется абстрактный класс Comparer
, который можно наследовать вместо реализации интерфейсов:
1 2 3 4 5 6 | public abstract class Comparer<T> : IComparer, IComparer<T> { public static Comparer<T> Default { get; } public abstract int Compare (T x, T y); int IComparer.Compare (object x, object y); } |
Пример реализации и использования компаратора упорядочения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // Класс, экземпляры которого будем сравнивать: class Wish { public string Name; public int Priority; public Wish (string name, int priority) { Name = name; Priority = priority; } } // Компаратор: class PriorityComparer : Comparer <Wish> { public override int Compare (Wish x, Wish y) { if (object.Equals (x, y)) return 0; return x.Priority.CompareTo (y.Priority); } } // Создаем и заполняем список желаний: var wishList = new List<Wish>(); wishList.Add (new Wish ("Peace", 2)); wishList.Add (new Wish ("Wealth", 3)); wishList.Add (new Wish ("Love", 2)); wishList.Add (new Wish ("3 more wishes", 1)); // Сортируем список с применением компаратора: wishList.Sort (new PriorityComparer()); // Выводим список: foreach (Wish w in wishList) Console.Write (w.Name + " | "); // Вывод: 3 more wishes | Love | Peace | Wealth | |
Компаратор также можно передавать в отсортированный словарь при его создании:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class SurnameComparer : Comparer <string> { string Normalize (string s) { s = s.Trim().ToUpper(); if (s.StartsWith ("MC")) s = "MAC" + s.Substring (2); return s; } public override int Compare (string x, string y) { return Normalize (x).CompareTo (Normalize (y)); } } var dic = new SortedDictionary<string,string> (new SurnameComparer()); dic.Add ("MacPhail", "second!"); dic.Add ("MacWilliam", "third!"); dic.Add ("McDonald", "first!"); foreach (string s in dic.Values) Console.Write (s + " "); // first! second! third! |
StringComparer
StringComparer
— это предопределенный компаратор эквивалентности и упорядочения для сравнения строк, позволяющий учитывать при сравнении язык и регистр. Он реализует оба интерфейса IEqualityComparer
and IComparer
(плюс их обобщенные версии), поэтому может использоваться с любыми словарями и коллекциями (отсортированными и нет).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public abstract class StringComparer : IComparer, IComparer <string>, IEqualityComparer, IEqualityComparer <string> { public abstract int Compare (string x, string y); public abstract bool Equals (string x, string y); public abstract int GetHashCode (string obj); public static StringComparer Create (CultureInfo culture, bool ignoreCase); public static StringComparer CurrentCulture { get; } public static StringComparer CurrentCultureIgnoreCase { get; } public static StringComparer InvariantCulture { get; } public static StringComparer InvariantCultureIgnoreCase { get; } public static StringComparer Ordinal { get; } public static StringComparer OrdinalIgnoreCase { get; } } |
Поскольку класс является статическим, экземпляры создаются через его статические свойства и методы.
Метод Create
возвращает экземпляр компаратора с указанной культурой, а параметр ignoreCase
указывает, стоит учитывать регистр или нет.
Свойства CurrentCulture
и CurrentCultureIgnoreCase
возвращают экземпляр компаратора, учитывающего текущую культуру (в соответствии с настройками компьютера), первый с учетом регистра, второй без.
Свойства InvariantCulture
и InvariantCultureIgnoreCase
аналогичны предыдущим, но применяют инвариантную культуру (соответствующую американской).
Свойства Ordinal
и OrdinalIgnoreCase
возвращают компаратор, выполняющий ординальное сравнение (в соответствии с порядком символов в таблице юникод), последний без учета регистра.
Свойство StringComparer.Ordinal
отражает стандартное поведение для сравнения эквивалентности, а свойство StringComparer.CurrentCulture
— для сравнения порядка.
Примеры:
1 2 3 4 5 6 | // Создание словаря с применением компаратора: var dict = new Dictionary<string, int> (StringComparer.OrdinalIgnoreCase); // Сортировка массива: string[] names = { "Tom", "HARRY", "sheila" }; CultureInfo ci = new CultureInfo ("en-AU"); Array.Sort<string> (names, StringComparer.Create (ci, false)); |
IStructuralEquatable и IStructuralComparable
Интерфейсы IStructuralEquatable
и IStructuralComparable
позволяют использовать структурную эквивалентность и структурное сравнение порядка при сравнении не структур: классов, массивов, кортежей. Структурная эквивалентность подразумевает равенство двух структур если все их поля равны. Определяются интерфейсы IStructuralEquatable
и IStructuralComparable
следующим образом:
1 2 3 4 5 6 7 8 9 | public interface IStructuralEquatable { bool Equals (object other, IEqualityComparer comparer); int GetHashCode (IEqualityComparer comparer); } public interface IStructuralComparable { int CompareTo (object other, IComparer comparer); } |
IStructuralEquatable
и IStructuralComparable
не являются интерфейсами компаратора, они должны реализовываться непосредственно сравниваемыми объектами (или хотя бы одним из них), а точнее сравниваемые объекты должны приводиться к этим интерфейсам. В этом случае объекты будут считаться равными, если все элементы в их составе равны:1 2 3 4 5 | int[] a1 = { 1, 2, 3 }; int[] a2 = { 1, 2, 3 }; IStructuralEquatable se1 = a1; // Приводим массив к интерфейсу IStructuralEquatable Console.Write (a1.Equals (a2)); // False Console.Write (se1.Equals (a2, EqualityComparer<int>.Default)); // True |