Comparing Objects in C # .NET

C # .NET offers many ways to compare objects, both instances of classes, and structures. There are so many ways that without streamlining these methods and understanding their proper use and implementation (if there is the possibility of redefinition), porridge will inevitably form in the head.

So, the System.Object class offers the following methods:
  • public static bool ReferenceEquals(object objA, object objB)
    {
        return objA == objB;
    }
             

  • public static bool Equals(object objA, object objB)
    {
        return objA == objB || (objA != null && objB != null && objA.Equals(objB));
    }
             

  • public virtual bool Equals(object obj)
    {
        return RuntimeHelpers.Equals(this, obj);
    }
             


Well and of course:
public static bool operator == (Foo left, Foo right);

It is also possible to inherit IEquatable, IStructuralEquatable.

ReferenceEquals

The ReferenceEquals method compares two links. If the references to the objects are identical, then returns true. This means that this method does not check instances for equality, but for identity. If you pass instances of a significant type to this method (even if you pass the same instance), it will always return false. This will happen because during the transfer there will be packaging of significant types and the links to them will be different.
Here, I would also like to mention the comparison of two lines by this method. For instance:
class Program
    {
        static void Main(string[] args)
        {
            string a = "Hello";
            string b = "Hello";
            if(object.ReferenceEquals(a,b))
                Console.WriteLine("Same objects");
            else
                Console.WriteLine("Not the same objects");
            Console.ReadLine();
        }
    }

Such a program can easily output “Same objects”. Do not worry, this is due to string interning. But this is a completely different story and there will be no talk about this.

public static bool Equals (object objA, object objB)

First, this method checks instances for identity, and if the objects are not identical, it checks them for null and delegates responsibility for comparing the overridden instance method Equals.

public virtual bool Equals (object obj)

By default, this method behaves exactly like ReferenceEquals. However, for significant types, it is overridden in System.ValueType as follows:
public override bool Equals(object obj)
		{
			if (obj == null)
			{
				return false;
			}
			RuntimeType runtimeType = (RuntimeType)base.GetType();
			RuntimeType left = (RuntimeType)obj.GetType();
			if (left != runtimeType)
			{
				return false;
			}
			if (ValueType.CanCompareBits(this))
			{
				return ValueType.FastEqualsCheck(this, obj);
			}
			FieldInfo[] fields = runtimeType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			for (int i = 0; i < fields.Length; i++)
			{
				object obj2 = ((RtFieldInfo)fields[i]).InternalGetValue(this, false);
				object obj3 = ((RtFieldInfo)fields[i]).InternalGetValue(obj, false);
				if (obj2 == null)
				{
					if (obj3 != null)
					{
						return false;
					}
				}
				else
				{
					if (!obj2.Equals(obj3))
					{
						return false;
					}
				}
			}
			return true;
		}

God forbid anyone to use such an implementation on large sets. BCL developers cannot know what significant types we will define and compare instances of significant types by their fields, using reflection, without knowing anything about these fields in advance. Of course, this is not a very productive way of comparison. Therefore, when using significant types known at the compilation stage, you must override this method, because who better than you can know how to compare the two objects you developed? For reference types, without the need to compare two instances in the manner of significant types, overriding this method is optional.
Let's look at an example of a competent redefinition of this method and immediately implement IEquatable:
class Vehicle:IEquatable
    {
        protected int speed;
        public int Speed
        {
            get { return this.speed; }
            set { this.speed = value; }
        }
        protected string name;
        public string Name
        {
            get { return this.name; }
            set { this.name = value; }
        }
        public Vehicle(){}
        public Vehicle(int speed, string name)
        {
            this.speed = speed;
            this.name = name;
        }
        public override bool Equals(object other)
        {
            //Последовательность проверки должна быть именно такой.
            //Если не проверить на null объект other, то other.GetType() может выбросить //NullReferenceException.            
            if (other == null)
                return false;
            //Если ссылки указывают на один и тот же адрес, то их идентичность гарантирована.
            if (object.ReferenceEquals(this, other))
                return true;
            //Если класс находится на вершине иерархии или просто не имеет наследников, то можно просто
            //сделать Vehicle tmp = other as Vehicle; if(tmp==null) return false; 
            //Затем вызвать экземплярный метод, сразу передав ему объект tmp.
            if (this.GetType() != other.GetType()) 
                return false;                                                                        
            return this.Equals(other as Vehicle);
        }
        public bool Equals(Vehicle other)
        {
            if (other == null)
                return false;
            //Здесь сравнение по ссылкам необязательно.
            //Если вы уверены, что многие проверки на идентичность будут отсекаться на проверке по ссылке - //можно имплементировать.
            if (object.ReferenceEquals(this, other))
                return true;
            //Если по логике проверки, экземпляры родительского класса и класса потомка могут считаться равными,
            //то проверять на идентичность необязательно и можно переходить сразу к сравниванию полей.
            if (this.GetType() != other.GetType())
                return false;
            if (string.Compare(this.Name, other.Name, StringComparison.CurrentCulture) == 0 && this.speed.Equals(other.speed))
                return true;
            else
                return false;
        }        
    }

The comment about the top of the hierarchy in redefining the virtual method is made for a reason. If you create an inheritor of the Vehicle class (for example, Bike), which will also have an overridden virtual Equals method, in which there will be no type comparison by GetType, and there will be an attempt to type conversion, Bike tmp = other as Bike; if(tmp!=null) this.Equals(tmp);then in this case, the following code may cause problems:
Vehicle vehicle = new Vehicle();
Bike bike = new Bike();
object vehicleObj = vehicle;
object bikeObject = bike;
bike.Equals(vehicleObj); //Базовый тип не сможет привестись к наследнику. Таким образом, может быть //нарушено свойство симметричности сравнения объектов


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

For meaningful types, you should always redefine it, just like virtual Equals (). For reference types, it is better not to override it, because, by default, from == on reference types, the behavior is expected as with the ReferenceEquals () method. So, everything is simple here.

IStructuralEquatable

IStructuralEquatable goes hand in hand with the IEqualityComparer interface. The IStructuralEquatable interface is implemented by classes such as System.Array or System.Tuple. According to Bill Wagner, IStructuralEquality declares that a type can constitute larger objects that implement the semantics of significant types and it is unlikely that we will ever need to implement it ourselves. Although, what is difficult in its implementation? Just look at its implementation in 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;
		}

Actually, the identity of the objects is checked first, then they are cast to the same type and compared in length. If the length is equal, then the elementwise comparison begins by delegating responsibility for this comparison to the Equals interface method (IEqualityComparer).

That's essentially all that can be said about comparing objects in C # .NET, but there remains one more small but important detail: the GetHashCode () method.

public virtual int GetHashCode ()

In general, the standard implementation of this method behaves like a unique identifier generator. The disadvantage of this approach is that the same semantically objects can return different hash values. Richter complains that the standard implementation is also low performing. A competent implementation of this method is very problematic. It is necessary to calculate hash quickly and have a large spread as a result so that there are no repetitions on sufficiently large sets. In fact, in most cases, the implementation of GetHashCode () is very simple. Everywhere shifts are made, “bitwise or,” or “exclusive or.” Richter himself gives an example with a structure that has two fields of type int. GetHashCode () it suggests to implement something like this:
internal sealed class Point
    {
        private int a;
        private int b;
        public override int  GetHashCode()
        {
            return a ^ b;
        }
    }

And here is how GetHasCode () is overridden in System.Char:
public override int GetHashCode()
		{
			return (int)this | (int)this << 16;
		}

Many examples can be given, and heuristic indicators are used almost everywhere for shifts that exclude or and so on.

When writing the article, well-known sources were used:
J.Richter, CLR via C #
B. Wagner Effective C #
Also, I used my experience and sources on the Internet, which does not make much sense.


Translation here

Also popular now: