Written by 13:15 Computer Environment, Languages & Coding

Comparing Objects by Value. Part 3: Type-specific Equals and Equality Operators

In Part 1 and Part 2, we have analyzed how to modify a class to compare objects by value.

Now, we will explore a type-specific implementation of how to compare objects by value including the IEquatable(Of T) generic interface and overload of “==” and “!=” operators.

Type-specific comparison of objects by value allows achieving:

  • a more stable, scalable and mnemonic (readable) code through overloaded operators;
  • higher performance.

We need to implement the type-specific comparison by value for the following reasons:

  • there should be IEquatable(Of T) implementation for all objects in standard Generic-collections (List(Of T), Dictionary(Of TKey, TValue), etc.)
  • a standard comparer EqualityComparer(Of T).Default uses the implementation of IEquatable(Of T) from operands.

The implementation of comparison of all ways to be simultaneously executed may lead to particular obstacles as it is necessary to ensure that:

  • correlation of comparison results in different methods including correlation in the inheritance;
  • minimization of using ‘copy-paste’ and total code amount;
  • comparison operators serve as static methods and have no polymorphism;
  • some CLS-compatible languages support operators or their overload.

I am going to describe on a particular example with the Person class how to compare objects by value using the type-specific implementation, taking into account the above-mentioned points.

In addition, I will provide a final example of the code with all the explanations on how it works.

Here is the Person class containing the implementation of the full set of objects by value:

using System;

namespace HelloEquatable
{
    public class Person : IEquatable<Person>
    {
        protected static int GetHashCodeHelper(int[] subCodes)
        {
            if ((object)subCodes == null || subCodes.Length == 0)
                return 0;

            int result = subCodes[0];

            for (int i = 1; i < subCodes.Length; i++)
                result = unchecked(result * 397) ^ subCodes[i];

            return result;
        }

        protected static string NormalizeName(string name) => name?.Trim() ?? string.Empty;

        protected static DateTime? NormalizeDate(DateTime? date) => date?.Date;

        public string FirstName { get; }

        public string LastName { get; }

        public DateTime? BirthDate { get; }

        public Person(string firstName, string lastName, DateTime? birthDate)
        {
            this.FirstName = NormalizeName(firstName);
            this.LastName = NormalizeName(lastName);
            this.BirthDate = NormalizeDate(birthDate);
        }

        public override int GetHashCode() => GetHashCodeHelper(
            new int[]
            {
                this.FirstName.GetHashCode(),
                this.LastName.GetHashCode(),
                this.BirthDate.GetHashCode()
            }
        );

        protected static bool EqualsHelper(Person first, Person second) =>
            first.BirthDate == second.BirthDate &&
            first.FirstName == second.FirstName &&
            first.LastName == second.LastName;

        public virtual bool Equals(Person other)
        {
            //if ((object)this == null)
            //    throw new InvalidOperationException("This is null.");

            if ((object)this == (object)other)
                return true;

            if ((object)other == null)
                return false;

            if (this.GetType() != other.GetType())
                return false;

            return EqualsHelper(this, other);
        }

        public override bool Equals(object obj) => this.Equals(obj as Person);

        public static bool Equals(Person first, Person second) =>
            first?.Equals(second) ?? (object)first == (object)second;

        public static bool operator ==(Person first, Person second) => Equals(first, second);

        public static bool operator !=(Person first, Person second) => !Equals(first, second);
    }
}
  1. The Person.GetHashCode() method determines an object hash code based on the fields where their combination forms a unique value of a particular object.
    You can find peculiarities of hash code determination and requirements to overriding the Object.GetHashCode() method in the documentation or in my first article.
  2. A static protected helper method EqualsHelper(Person, Person) compares two objects by fields where their combination forms a unique value of a particular object.
  3. A virtual Person.Equals(Person) method implements the IEquatable(Of Person) interface. I declare this method to be virtual, as it will be necessary to override it in the inheritance.
  • A zero step describes the code comments that checks a null reference to the current object. If the reference equals null, then InvalidOperationException is thrown, indicating that the object is in an invalid state.
  • The first step checks for the equality by reference of the current and specified objects. If it is true, the objects are identical.
  • Then, we check a null reference to the specified object. When it is null, then objects are not equal.Before we check the equality by reference with == and != operators or the Object.ReferenceEquals(Object, Object) method, we should cast operands to objects to call for an unloaded operator. Additionally, we must cast operands to objects if we use == and != operators. It is necessary to do this, as operators in this class are overloaded and will use the Person.Equals(Person) method.
  • Then, it is possible to check if types of the current and specified objects are identical. If it is false, then objects are not equal.We need to check the identity of objects types, rather than their compatibility in order to ensure the comparison by value when inheriting a type.
  • If previous checks have not declared whether objects are equal or not, it is better to check current and specified objects by value using the EqualsHelper(Person, Person) helper method.
  1. The Person.Equals(Object) method is implemented as the Person.Equals(Person) method by casting the specified object to the Person type with an ‘as’ operator.
    Note:If object types are not compatible, then cast returns null.  In this case, the objects are not equal in the Person.Equals(Person) method.If object types are not compatible, then cast returns null.  In this case, the objects are not equal in the Person.Equals(Person) method.However, in general, we can get a comparison result in the Person.Equals(Person) method when the objects are equal, as it may be possible to call an instance method without creating an instance. Then, if a reference to the current and specified objects equals null as well as their types are incompatible, then the call Person.Equals(Object), followed by Person.Equals(Person), will lead to the incorrect result – objects are equal, though they are not equal actually.

    This case does not seem to require much processing, as there is no sense in calling an instance method and using its result without creating the instance itself.

    Still, if we need to take it into account, then we may uncomment the code at the zero step in the Person.Equals(Person) method that will prevent us from receiving the incorrect result when calling the Person.Equals(Object) method, but also will throw a more informative exception instead of NullReferenceException when directly calling the Person.Equals(Person) method from the null-object.

  1. To support static comparison of objects by value for CLS-compatible languages that do not support operators or their overload, we may use the static method Equals(Person, Person) as an alternative to the Object.Equals(Object, Object) method.
  • The Person.Equals(Person, Person) method is implemented by calling the instance virtual method Person.Equals(Person). We need to do this so that the call x == y has the same result as “y == x” which refers to the corresponding requirement.
  • Since it is not possible to override static methods with type inheritance, then the reason for this implementation is the call of the static method Person.Equals(Person, Person) via the call of the virtual instance method Person.Equals(Person) – a necessity to achieve polymorphism in static calls. Thus, in this case, the result of static will match the result of instance comparison in inheritance.
  • In the Person.Equals(Person, Person) method, a call to the instance method Person.Equals(Person) is implemented with the null reference to the object that calls for this method Equals(Person).If this object is null, then a comparison of objects by reference is possible.
  1. A call of the static method Person.Equals(Person, Person) (for the operator “!=” with the operator !) is used to implement the overloaded operators Person.==(Person, Person) and Person.!=(Person, Person).

Thus, we have found a correct and productive way to implement comparison of objects by value in one class, as well as taken into account the correctness behavior for inheritance.

In addition, it is necessary to analyze how to correctly implement the inheritance for comparing objects by value, if we add a field in the inherited class included into multiple object fields that form a unique value of the object:

Assume we have the PersonEx class that inherits the Person class and has an additional property MiddleName. In this case, the comparison of two objects of the PersonEx class

John Teddy Smith 1990-01-01 
John Bobby Smith 1990-01-01

will lead to the object equality, which is incorrect.

Therefore, though this task seems to be trivial in addition to costs and risks, the implementation of comparing objects by value in the current .NET framework tends to result in including comparison into the inherited classes which leads to additional costs and errors.

We will talk about a possible solution of this task in my further publications.

Also read:

Comparing Objects by Value. Part 1: Beginning

Comparing Objects by Value. Part 2: Implementation Notes of the Equals Method

Comparing Objects by Value. Part 4: Inheritance & Comparison Operators

Comparing Objects by Value. Part 5: Structure Equality Issue

Comparing Objects by Value. Part 6: Structure Equality Implementation

Tags: Last modified: September 23, 2021
Close