Written by 12:27 Computer Environment, Languages & Coding

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

In the previous article, we analyzed how to compare objects by value on a particular example with the Person class that includes:

Each method returns the same result:

Person p1 = new Person("John", "Smith", new DateTime(1990, 1, 1));
Person p2 = new Person("John", "Smith", new DateTime(1990, 1, 1));
//Person p2 = new Person("Robert", "Smith", new DateTime(1991, 1, 1));

object o1 = p1;
object o2 = p2;

bool isSamePerson;

isSamePerson = o1.Equals(o2);
isSamePerson = p1.Equals(p2);
isSamePerson = object.Equals(o1, o2);
isSamePerson = Person.Equals(p1, p2);
isSamePerson = p1 == p2;
isSamePerson = !(p1 == p2);

In addition, I would like to add that each method should be commutative:

x.Equals(y) returns the same result as y.Equals(x)

Thus, a client-side code may compare objects by using any methods. In any case, the result will be deterministic.

Still, it is necessary to determine how we may ensure determinacy of the result when implementing static methods and comparison operators in the case of inheritance. Additionally, we should take into account that static methods and operators have no polymorphic behavior.

To determine it, I will use the following example with the Person class:

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);
    }
}

Also, I will create the derived class PersonEx:

using System;

namespace HelloEquatable
{
    public class PersonEx : Person, IEquatable<PersonEx>
    {
        public string MiddleName { get; }

        public PersonEx(
            string firstName, string middleName, string lastName, DateTime? birthDate
        ) : base(firstName, lastName, birthDate)
        {
            this.MiddleName = NormalizeName(middleName);
        }

        public override int GetHashCode() => GetHashCodeHelper(
            new int[]
            {
                base.GetHashCode(),
                this.MiddleName.GetHashCode()
            }
        );

        protected static bool EqualsHelper(PersonEx first, PersonEx second) =>
            EqualsHelper((Person)first, (Person)second) &&
            first.MiddleName == second.MiddleName;

        public virtual bool Equals(PersonEx 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(Person other) => this.Equals(other as PersonEx);

        // Optional overloadings:

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

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

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

        public static bool operator !=(PersonEx first, PersonEx second) => !Equals(first, second);
    }
}

As you can see, we have added the MiddleName property. It means that it is necessary:

  • To implement the IEquatable(Of PersonEx) interface.
  • To implement the PersonEx.Equals(Person) method, overriding the inherited method Person.Equals(Person) and casting the object of the Person type to the object of the PersonEx type.
    Note: the latter one has been declared as virtual to have inheritance be possible.

Otherwise, objects comparison, where all key fields (except MiddleName) are equal, will return the result ‘objects are equal’, which is incorrect.

It should be noted that:

  • The implementation of the PersonEx.Equals(PersonEx) method is identical to the one of the Person.Equals(Person) method.
  • The implementation of the PersonEx.Equals(Person) method is identical to the one of the Person.Equals(Object) method.
  • The implementation of the static protected method EqualsHelper(PersonEx, PersonEx) is identical to the one of the EqualsHelper(Person, Person) method.

Then, we implement the PersonEx.Equals(Object) method that overrides the inherited method Equals(Object) and represents a call of the PersonEx.Equals(PersonEx) method with casting the specified object to the PersonEx type with the as operator.

Note:
The implementation of the PersonEx.Equals(Object) method is not a must. If we have not done it, but called the Equals(Object) method by a client-side code, the inherited method Person.Equals(Object) would be used that calls the virtual method PersonEx.Equals(Person) casting to the PersonEx.Equals(PersonEx) method.

However, the PersonEx.Equals(Object) method is implemented for the completeness of the code and its faster performance by minimizing the number of type casts and intermediate calls of methods.

Now, it does not matter what method for the object of the PersonEx class we will call: Equals(PersonEx), Equals(Person), or Equals(object); as the same objects will return the same result.

Polymorphism allows such a behavior.

In addition, we have implemented the static method PersonEx.Equals(PersonEx, PersonEx), as well as its corresponding comparison operators PersonEx.==(PersonEx, PersonEx) and PersonEx.!=(PersonEx, PersonEx) in the PersonEx class.

Using the PersonEx.Equals(PersonEx, PersonEx) method, as well as the PersonEx.==(PersonEx, PersonEx) and PersonEx.!=(PersonEx, PersonEx) operators for the same objects, will return the same result as using the Equals instance methods of the PersonEx type.

The more we move on, the more interesting it becomes.

As you can see, the PersonEx class has inherited the static method Equals(Person, Person) and corresponding comparison operators ==(Person, Person) and  !=(Person, Person) from the Person class.

I wonder, what result the following code would return.

bool isSamePerson;

PersonEx pex1 = new PersonEx("John", "Teddy", "Smith", new DateTime(1990, 1, 1));
PersonEx pex2 = new PersonEx("John", "Bobby", "Smith", new DateTime(1990, 1, 1));
//PersonEx pex2 = new PersonEx("John", "Teddy", "Smith", new DateTime(1990, 1, 1));
Person p1 = pex1;
Person p2 = pex2;

isSamePerson = Person.Equals(pex1, pex2);
isSamePerson = PersonEx.Equals(p1, p2);
isSamePerson = pex1 == pex2;
isSamePerson = p1 == p2;

Although the Equals(Person, Person) method and comparison operators ==(Person, Person) and !=(Person, Person) are static, they will always return the same result when calling for the Equals(PersonEx, PersonEx) method, the ==(PersonEx, PersonEx) and !=(PersonEx, PersonEx) operators, or any instance virtual Equals methods.

Thus, to get such a polymorphic behavior, it is necessary to implement the Equals static methods and the “==” and “!=” comparison operators using the instance virtual Equals method.

Furthermore, the implementation of the Equals(PersonEx, PersonEx) and the ==(PersonEx, PersonEx) and !=(PersonEx, PersonEx) operators in the PersonEx class is optional. The same is applied for the PersonEx.Equals(Object) method.

The Equals(PersonEx, PersonEx) methods, as well as the ==(PersonEx, PersonEx) and !=(PersonEx, PersonEx) operators are implemented for the completeness of the code and its faster performance by minimizing the number of type casts and intermediate calls of methods.

The only difference in the polymorphism of the Equals static methods and the “==” and “!=” operators is that if we cast two objects of the Person or PersonEx type to the Object type, then it will be possible to compare objects by reference with the == and != operators, and by value – using the Object.Equals(Object, Object) method. But, it depends on the platform design.

In my further publication, we will consider peculiarities of comparison by value for the objects and structure instances. In addition, we will analyze cases when it is a good idea to implement comparison of objects by value and how it can be achieved.

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 3: Type-specific Equals and Equality 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