2 29.7K ru

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, тк он переопределен. Механизм выглядит следующим образом:

equals flowchart

У типа 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 типы:

  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.

Пример реализации:

 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 прост:

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

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

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

  • При реализации метода Equals всегда реализуйте метод GetHashCode. При этом сохраняется синхронизация методов Equals и GetHashCode.
  • Всегда переопределяйте метод Equals при реализации оператора равенства (==). При этом необходимо, чтобы и оператор, и метод выполняли одну функцию. Это позволит специфическим типам, таким как Hashtable и ArrayList, использующему метод Equals, работать таким же образом, как и переопределенная операция равенства.
  • Переопределяйте метод Equals каждый раз при реализации IComparable.
  • При реализации интерфейса IComparable рекомендуется реализовать перегрузку оператора для операторов "равенство" (==), "неравенство" (!=), "меньше чем" (<) и "больше чем" (>).
  • Не вызывайте исключения из методов Equals и GetHashCode или оператора равенства (==).

Источники и дополнительная информация:

  1. Сравнение объектов в C#.NET
  2. Чем отличаются оператор == и вызов метода object.Equals в C#?
  3. ValueType.Equals(Object)
  4. Правила реализации метода Equals и оператора равенства (==)
  5. System.Object
  6. ValueType

Comments:

Please log in to be able add comments.
mcfly, вы правы в том что его можно использовать в других LINQ выражениях, о чем я упоминал в статье, но тк статья сосредоточена на Equals и как сравнивать типы, то пример в этом ключе рассматривался.
В коде с IEqualityComparer написана чушь. Работает IEqualityComparer как-то так : public class IntEqualityComparer : IEqualityComparer<int> { public bool Equals(int x, int y) { return x == y; } public int GetHashCode([DisallowNull] int obj) { return obj.GetHashCode(); } } public void Main() { int[] vals = { 1, 3, 1, 4, 3, 5, 12, 4, 4, 4 }; Console.WriteLine(string.Join(", ", vals.Distinct(new IntEqualityComparer()))); } на выходе будет : 1, 3, 4, 5, 12 Т.е. сам класс, реализующий IEqualityComparer можно подкидывать в разные Linq выражения (Sort, Distinct и т.д.) и прочие места, где требуется IEqualityComparer и сравниваться будет так, как реализовано в вашем классе.