Equals c что делает

от admin

Equals() и как работает сравнение типов в C#

Правильное сравнивание типов в .NET всегда является проблемой не только для новичков, но и для опытных разработчиков. Сегодня мы рассмотрим как правильно сравнивать ссылочные (reference) и значимые (value) типы в .NET.

Сравнение типов через Equals() и == в Reference Type

Как работает Equals() в C#

В классе System.Object есть все несколько методов, один из них ReferenceEquals

Стоить отметить, что == и RefefenceEquals работает идентично, сравнивая ссылки на объекты в heap'е, а значит при выполнении данного кода

На консоль будет выведено:

Как работает метод virtual bool Equals, обратимся к исходному коду .NET Framework'a

По сути за этим для стандартной реализации object.Equals скрывается вызов ReferenceEquals под капотом.

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

Например, перепишем наш старый пример, где переопределим метод Equals :

На экране мы увидим:

Несмотря на то, что obj1 и obj2 являются типом Object , происходит вызов Equals класса TestClass , тк он переопределен. Механизм выглядит следующим образом:

equals flowchart

У типа Object есть еще один статический метод static bool Equals(Object objA, Object objB)

Его исходный код выглядит следующим образом:

Этот является оберткой виртуального метода Equals, но перед его вызовом, делает проверку равенства ссылок путем == и проверку на null каждого из объектов проверки.

Сравнение Value типов через Equals

Метод ValueType.Equals(Object) переопределяет Object.Equals(Object) и реализует свою функцию, вот исходный код с .NET Framework:

Давайте разберемся как этот метод сравнивает value типы:

  1. Проверяет можно ли сделать побитовое сравнение, если да, то выполняется FastEqualsCheck(Object a, Object b);
  2. Если же это не побитовое сравнение, применяется рефлексия, и значения всех полей сравниваются попарно через Equals(object).

Совет:

Если структура содержит поля, которые являются ссылочными типами, следует переопределить метод Equals(Object) . Это может повысить производительность и позволить более точно представить значение равенства для типа. Так как при большом количестве полей, выполнение ValueType.Equals может быть очень затратным по времени. При переопределении так же не забывайте переопределять и метод GetHashCode который используется в таких типах данных как Dictionary для сравнения объектов.

Сравнение с помощью вспомогательных классов и интерфейсов

IEqualityComparer

Если вам нужен метод для сравнения объектов для конкретного типа, а не общей ситуации (или вы хотите сравнивать специальным образом объекты, которые вам не принадлежат), вы можете делегировать сравнение специальному объекту, реализующему интерфейс IEqualityComparer или типизированный IEqualityComparer<T> . Сравнение при помощи таких сравнивающих объектов применяют, например, Hashatable и Dictionary<K, V>, а также некоторые LINQ-методы.

К этому интерфейсу есть и его реализация с помощью EqualityComparer<T> . EqualityComparer<T>, он проверяет реализует ли объект интерфейс IEquatable<T> , и в противном случае выполняет сравнение через стандартный Equal(object ). При реализации IEqualityComparer рекомендуется наследоваться от EqualityComparer.

IComparable

Проверка на равенство так же может быть выполнена через сравнение больше/меньше/равно. Для этого используются операторы сравнения </> , интерфейсы IComparable (аналог метода Equals(object) ), IComparable<T> (аналог интерфейса IEquatable<T>)

IStructuralEquatable

IStructuralEquatable работает в паре с интерфейсом IEqualityComparer . Интерфейс IStructuralEquatable реализуют такие классы как System.Array или System.Tuple . IStructuralEquality декларирует то, что тип может составлять более крупные объекты, которые реализуют семантику значимых типов и вряд ли когда-либо нам потребуется его самостоятельно реализовывать.

Пример реализации этого интерфейса можно подсмотреть в System.Array :

Алгоритм реализации такого интерфейса в System.Array прост:

  1. Проверяется объект other на null;
  2. проверяется равенство ссылок;
  3. Поэлементное сравнение с помощью IEqualityComparer.Equals;

Правила реализации метода Equals и оператора равенства (==)

При реализации метода Equals и оператора равенства (==) следует руководствоваться следующим:

Сравнение объектов в C#.NET

Также имеется возможность наследования IEquatable, IStructuralEquatable.

ReferenceEquals

Метод ReferenceEquals сравнивает две ссылки. Если ссылки на объекты идентичны, то возвращает true. Это значит, что данный метод проверяет экземпляры не на равенство, а на тождество. В случае передачи этому методу экземпляров значимого типа (даже если передать один и тот же экземпляр) всегда будет возвращать false. Так произойдёт потому, что при передаче произойдёт упаковка значимых типов и ссылки на них будут разные.
Здесь также хотелось бы упомянуть о сравнение двух строк этим методом. Например:

Такая программа, запросто может вывести «Same objects». Не стоит переживать, это связано с интернированием строк. Но это совсем другая история и здесь об этом речи идти не будет.

public static bool Equals(object objA, object objB)

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

public virtual bool Equals(object obj)

По умолчанию, этот метод ведёт себя точно также как ReferenceEquals. Однако для значимых типов он переопределён и в System.ValueType выглядит следующим образом:

Не дай бог никому пользоваться такой имплементацией на больших множествах. Разработчики BCL не могут знать какие значимые типы мы будем определять и проводят сравнение экземпляров значимых типов по их полям, используя рефлексию, заранее ничего не зная об этих полях. Разумеется, это не очень производительный способ сравнения. Поэтому, при использовании значимых типов, известных на этапе компиляции, необходимо переопределить этот метод, ибо кто лучше вас может знать как сравнить два разработанных вами объекта? Для ссылочных типов, без нужды сравнения двух экземпляров на манер значимых типов, переопределять этот метод необязательно.
Посмотрим на пример грамотного переопределения этого метода и сразу реализуем IEquatable:

Комментарий про вершину иерархии в переопределении виртуального метода сделан не просто так. Если создать наследник класса Vehicle (например, Bike), который также будет иметь переопределённый виртуальный метод Equals, в котором не будет сравнения типов по GetType, а будет попытка приведения типа Bike tmp = other as Bike; if(tmp!=null) this.Equals(tmp); то в таком случае, следующий код может вызывать проблемы:

public static bool operator == (Foo left, Foo right)

Для значимых типов всегда следует переопределять, как и виртуальный Equals(). Для ссылочных типов лучше не переопределять, ибо, по умолчанию, от == на ссылочных типах ожидается поведение как у метода ReferenceEquals(). Так что, здесь всё просто.

IStructuralEquatable

IStructuralEquatable идёт рука об руку с интерфейсом IEqualityComparer. Интерфейс IStructuralEquatable реализуют такие классы как System.Array или System.Tuple. Как пишет Билл Вагнер, IStructuralEquality декларирует то, что тип может составлять более крупные объекты, которые имплементируют семантику значимых типов и вряд ли когда-либо нам потребуется его самостоятельно реализовывать. Хотя, что сложного в его реализации? Достаточно посмотреть на его реализацию в System.Array:

Собственно, сначала проверяется тождественность объектов, затем производится приведение к одному типу и сравнение по длине. Если длинна равна, то тогда начинается поэлементное сравнение через делегирование ответственности за это сравнение интерфейсному (IEqualityComparer) методу Equals.

Читать:
При котором код будет допускать однозначное декодирование

Вот, по сути, и всё, что можно сказать о сравнении объектов в C#.NET, но осталась ещё одна маленькая, но важная деталь: метод GetHashCode().

public virtual int GetHashCode()

В общем и целом, стандартная реализация этого метода ведёт себя как генератор уникального идентификатора. Минус такого подхода состоит в том, что одинаковые семантически объекты, могут возвращать разные hash-значения. Рихтер жалуется на то, что стандартная реализация ещё и низкопроизводительна. Грамотная реализация этого метода весьма проблематична. Необходимо высчитывать hash быстро и иметь большой разброс в результате, чтобы не случалось повторений на достаточно больших множествах. На самом деле, в большей части случаев, имплементации GetHashCode() донельзя простые. Везде производятся сдвиги, «побитовые или», или «исключающие или». Сам Рихтер приводит пример со структурой, имеющей два поля типа int. GetHashCode() он предлагает имплементировать примерно так:

А вот как переопределён GetHasCode() в System.Char:

Можно приводить много примеров и практически везде используются эвристические показатели для сдвигов, исключающих или и так далее.

Класс Object

В C# предусмотрен специальный класс object, который неявно считается базовым классом для всех остальных классов и типов, включая и типы значений. Иными словами, все остальные типы являются производными от object. Это, в частности, означает, что переменная ссылочного типа object может ссылаться на объект любого другого типа. Кроме того, переменная типа object может ссылаться на любой массив, поскольку в C# массивы реализуются как объекты. Формально имя object считается в C# еще одним обозначением класса System.Object, входящего в библиотеку классов для среды .NET Framework.

Практическое значение этого в том, что помимо методов и свойств, которые вы определяете, также появляется доступ к множеству общедоступных и защищенных методов-членов, которые определены в классе Object. Эти методы присутствуют во всех определяемых классах.

Методы System.Object

Ниже перечислены все методы данного класса:

ToString()

Метод ToString() возвращает символьную строку, содержащую описание того объекта, для которого он вызывается. Кроме того, метод ToString() автоматически вызывается при выводе содержимого объекта с помощью метода WriteLine(). Этот метод переопределяется во многих классах, что позволяет приспосабливать описание к конкретным типам объектов, создаваемых в этих классах.

Применяйте этот метод, когда нужно получить представление о содержимом объекта — возможно, в целях отладки. Он предлагает очень ограниченные средства форматирования данных. Например, даты в принципе могут быть отображены в огромном разнообразии форматов, но DateTime.ToString() не оставляет никакого выбора в этом отношении. Если нужно более сложное строковое представление, которое, например, принимает во внимание установленные предпочтения или местные стандарты, то понадобится реализовать интерфейс IFormattable.

GetHashCode()

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

Хеш-код можно использовать в любом алгоритме, где хеширование применяется в качестве средства доступа к хранимым объектам. Следует, однако, иметь в виду, что стандартная реализация метода GetHashCode() не пригодна на все случаи применения.

Equals() и ReferenceEquals()

По умолчанию метод Equals (object) определяет, ссылается ли вызывающий объект на тот же самый объект, что и объект, указываемый в качестве аргумента этого метода, т.е. он определяет, являются ли обе ссылки одинаковыми. Метод Equals (object) возвращает логическое значение true, если сравниваемые объекты одинаковы, в противном случае — логическое значение false. Он может быть также переопределен в создаваемых классах. Это позволяет выяснить, что же означает равенство объектов для создаваемого класса. Например, метод Equals (object) можно определить таким образом, чтобы в нем сравнивалось содержимое двух объектов.

Как несложно догадаться, учитывая существование трех различных методов сравнения объектов, среда .NET использует довольно сложную схему определения эквивалентности объектов. Следует учитывать и использовать тонкие различия между этими тремя методами и операцией сравнения ==. Кроме того, также существуют ограничения, регламентирующие, как следует переопределять виртуальную версию Equals() с одним параметром, если вы решитесь на это — поскольку некоторые базовые классы из пространства имен System.Collections вызывают этот метод и ожидают от него определенного поведения.

Finalize()

Назначение этого метода в C# примерно соответствует деструкторам С++, и он вызывается при сборке мусора для очистки ресурсов, занятых ссылочным объектом. Реализация Finalize() из Object на самом деле ничего не делает и игнорируется сборщиком мусора. Обычно переопределять Finalize() необходимо, если объект владеет неуправляемыми ресурсами, которые нужно освободить при его уничтожении. Сборщик мусора не может сделать это напрямую, потому что он знает только об управляемых ресурсах, поэтому полагается на финализацию, определенную вами.

GetType()

Этот метод возвращает экземпляр класса, унаследованный от System.Type. Этот объект может предоставить большой объем информации о классе, членом которого является ваш объект, включая базовый тип, методы, свойства и т.п. System.Type также представляет собой стартовую точку технологии рефлексии .NET.

Clone()

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

Давайте рассмотрим применение некоторых из этих методов на конкретном примере:

C#: тип объект (Object Type)

object ( System.Object ) — окончательный базовый класс для всех типов. Любой тип может быть автоматически (скрыто) приведен к базовому классу (upcast) object .

Эта особенность может быть очень полезна при создании универсальных хранилищ, например:

Поскольку Stack работает с типом object , в него можно добавлять ( Push ) и удалять ( Pop ) экземпляры любого типа.

object — ссылочный тип, но не смотря на это значимые типы также могут быть приведены к типу object (и обратно из него). Чтобы сделать это возможным, CLR должна проделать ряд приготовлений, чтобы устранить различия между ссылочными и значимыми типами. Этот процесс называется приведение к объектному типу (boxing) и восстановление значения из объектного образа (unboxing).

Приведение к объектному типу (boxing) и восстановление значения из объектного образа (unboxing)

Приведение к объектному типу (boxing) — процесс приведения экземпляра значимого типа к ссылочному типу. Ссылочным типом в данном случае может быть либо класс object , либо интерфейс. Восстановление значения из объектного образа (unboxing) — обратный процесс, приведение object назад к изначальному значимому типу.

Похожие статьи