Equals() и как работает сравнение типов в C#
Правильное сравнивание типов в .NET всегда является проблемой не только для новичков, но и для опытных разработчиков. Сегодня мы рассмотрим как правильно сравнивать ссылочные (reference) и значимые (value) типы в .NET.
Сравнение типов через Equals() и == в Reference Type
Как работает Equals() в C#
В классе System.Object
есть все несколько методов, один из них ReferenceEquals
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[System.Runtime.Versioning.NonVersionable]
public static bool ReferenceEquals (Object objA, Object objB)
{
return objA == objB;
}
Стоить отметить, что ==
и RefefenceEquals
работает идентично, сравнивая ссылки на объекты в heap'е, а значит при выполнении данного кода
public class Program
{
public static async Task Main()
{
var obj1 = new TestClass
{
Count = 2
};
var obj2 = new TestClass
{
Count = 2
};
Console.WriteLine(obj1 == obj2);
Console.WriteLine(obj1.Equals(obj2));
Console.WriteLine(object.ReferenceEquals(obj1, obj2));
Console.ReadKey();
}
}
class TestClass
{
public int Count { get; set; }
}
На консоль будет выведено:
False
False
False
Как работает метод virtual bool Equals,
обратимся к исходному коду .NET Framework'a
public virtual bool Equals(Object obj)
{
return RuntimeHelpers.Equals(this, obj);
}
По сути за этим для стандартной реализации object.Equals
скрывается вызов ReferenceEquals под капотом.
При этом определение того, какой метод Equals
вызвать определяется на динамическом типе левого аргумента.
Например, перепишем наш старый пример, где переопределим метод Equals
:
public class Program
{
public static async Task Main()
{
object obj1 = new TestClass
{
Count = 2
};
object obj2 = new TestClass
{
Count = 2
};
Console.WriteLine(obj1 == obj2);
Console.WriteLine(obj1.Equals(obj2));
Console.WriteLine(object.ReferenceEquals(obj1, obj2));
}
}
class TestClass
{
public int Count { get; set; }
public override bool Equals(object obj)
{
if (obj is TestClass objectType)
{
return this.Count == objectType.Count;
}
return false;
}
}
На экране мы увидим:
False
True
False
Несмотря на то, что obj1
и obj2
являются типом Object
, происходит вызов Equals класса TestClass
, тк он переопределен. Механизм выглядит следующим образом:
У типа Object есть еще один статический метод static bool Equals(Object objA, Object objB)
Его исходный код выглядит следующим образом:
public static bool Equals(Object objA, Object objB)
{
if (objA==objB) {
return true;
}
if (objA==null || objB==null) {
return false;
}
return objA.Equals(objB);
}
Этот является оберткой виртуального метода Equals, но перед его вызовом, делает проверку равенства ссылок путем == и проверку на null каждого из объектов проверки.
Сравнение Value типов через Equals
Метод ValueType.Equals(Object)
переопределяет Object.Equals(Object)
и реализует свою функцию, вот исходный код с .NET Framework:
[Serializable]
[System.Runtime.InteropServices.ComVisible(true)]
public abstract class ValueType {
[System.Security.SecuritySafeCritical]
public override bool Equals (Object obj) {
BCLDebug.Perf(false, "ValueType::Equals is not fast. "+this.GetType().FullName+" should override Equals(Object)");
if (null==obj) {
return false;
}
RuntimeType thisType = (RuntimeType)this.GetType();
RuntimeType thatType = (RuntimeType)obj.GetType();
if (thatType!=thisType) {
return false;
}
Object thisObj = (Object)this;
Object thisResult, thatResult;
// if there are no GC references in this object we can avoid reflection
// and do a fast memcmp
if (CanCompareBits(this))
return FastEqualsCheck(thisObj, obj);
FieldInfo[] thisFields = thisType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
for (int i=0; i<thisFields.Length; i++) {
thisResult = ((RtFieldInfo)thisFields[i]).UnsafeGetValue(thisObj);
thatResult = ((RtFieldInfo)thisFields[i]).UnsafeGetValue(obj);
if (thisResult == null) {
if (thatResult != null)
return false;
}
else
if (!thisResult.Equals(thatResult)) {
return false;
}
}
return true;
}
Давайте разберемся как этот метод сравнивает value типы:
- Проверяет можно ли сделать побитовое сравнение, если да, то выполняется
FastEqualsCheck(Object a, Object b);
- Если же это не побитовое сравнение, применяется рефлексия, и значения всех полей сравниваются попарно через 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.
Пример реализации:
public class Program
{
public static async Task Main()
{
var obj1 = new TestClass
{
Count = 2
};
var obj2 = new TestClass
{
Count = 2
};
Console.WriteLine(obj1.Equals(obj2));
}
}
class TestClass : IEqualityComparer<int>
{
public int Count { get; set; }
public bool Equals(int x, int y)
{
return x == y;
}
public int GetHashCode(int obj)
{
return obj.GetHashCode();
}
}
IComparable
Проверка на равенство так же может быть выполнена через сравнение больше/меньше/равно. Для этого используются операторы сравнения </>
, интерфейсы IComparable
(аналог метода Equals(object)
), IComparable<T>
(аналог интерфейса IEquatable<T>)
Пример реализации:
public class Program
{
public static async Task Main()
{
var obj1 = new TestClass
{
Count = 2
};
var obj2 = new TestClass
{
Count = 2
};
Console.WriteLine(obj1.CompareTo(obj2));
}
}
class TestClass : IComparable<TestClass>
{
public int Count { get; set; }
public int CompareTo(TestClass other)
{
return ReferenceEquals(this, other) ? 0 : Count.CompareTo(other.Count);
}
}
IStructuralEquatable
IStructuralEquatable
работает в паре с интерфейсом IEqualityComparer
. Интерфейс IStructuralEquatable
реализуют такие классы как System.Array
или System.Tuple
. IStructuralEquality
декларирует то, что тип может составлять более крупные объекты, которые реализуют семантику значимых типов и вряд ли когда-либо нам потребуется его самостоятельно реализовывать.
Пример реализации этого интерфейса можно подсмотреть в System.Array
:
bool IStructuralEquatable.Equals(object other, IEqualityComparer comparer)
{
if (other == null)
{
return false;
}
if (object.ReferenceEquals(this, other))
{
return true;
}
Array array = other as Array;
if (array == null || array.Length != this.Length)
{
return false;
}
for (int i = 0; i < array.Length; i++)
{
object value = this.GetValue(i);
object value2 = array.GetValue(i);
if (!comparer.Equals(value, value2))
{
return false;
}
}
return true;
}
Алгоритм реализации такого интерфейса в System.Array
прост:
- Проверяется объект other на null;
- проверяется равенство ссылок;
- Поэлементное сравнение с помощью
IEqualityComparer.Equals;
Правила реализации метода Equals и оператора равенства (==)
При реализации метода Equals и оператора равенства (==) следует руководствоваться следующим:
- При реализации метода
Equals
всегда реализуйте методGetHashCode
. При этом сохраняется синхронизация методовEquals
иGetHashCode
. - Всегда переопределяйте метод
Equals
при реализации оператора равенства (==). При этом необходимо, чтобы и оператор, и метод выполняли одну функцию. Это позволит специфическим типам, таким какHashtable
и ArrayList, использующему методEquals
, работать таким же образом, как и переопределенная операция равенства. - Переопределяйте метод Equals каждый раз при реализации
IComparable
. - При реализации интерфейса
IComparable
рекомендуется реализовать перегрузку оператора для операторов "равенство" (==), "неравенство" (!=), "меньше чем" (<) и "больше чем" (>). - Не вызывайте исключения из методов Equals и GetHashCode или оператора равенства (==).
Источники и дополнительная информация: