Written by 11:12 Computer Environment, Languages & Coding

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

In the previous article, we have reviewed a general concept of implementing a minimum set of required modifications that include overriding the Object.Equals(Object) and Object.GetHashCode() methods in order to compare class objects by value on a standard .NET framework.

Let’s consider the implementation features of the Object.Equals(Object) method so that it meets the following documentation requirement:

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

The Person class we have implemented in the previous article contains Equals(Object) implementation:

public override bool Equals(object obj)
{
    if ((object)this == obj)
        return true;

    var other = obj as Person;

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

    return EqualsHelper(this, other);
}

As a first step, it compares the reference equality of the current and specified objects. In case the comparison fails, the specified object is being cast to the Person type, so we can compare objects by value.

An example in the official documentation uses the as operator. Let’s check its correctness by inheriting PersonEx from Person. We add the new Middle Name property to the PersonEx class and override the Person.Equals(Object) and Person.GetHashCode() in an appropriate manner.

using System;

namespace HelloEquatable
{
    public class PersonEx : Person
    {
        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() =>
            base.GetHashCode() ^
            this.MiddleName.GetHashCode();

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

        public override bool Equals(object obj)
        {
            if ((object)this == obj)
                return true;

            var other = obj as PersonEx;

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

            return EqualsHelper(this, other);
        }
    }
}

If we call the Equals(Object) method for the Person type object and pass there a PersonEx object, we get the following — if two objects have the same First Name, Last Name, and the Birth Date, the Equals method returns true, otherwise false.

PersonEx type will be cast to the type of Person using the as operator, and Further objects will be compared to the values of the fields, available only in the Person class.

It is obvious that it is not a correct behavior:

Even though First Name, Last Name, and a date birth match, it does not mean that it is the same person. One person does not have the Middle Name attribute (means the attribute itself, not its value), whereas another one has. Thus, these entities refer to different types.

If we call the Equals method for the PersonEx type object and pass there a Person object, then the Equals method returns false, regardless of the object properties values. In this case, the Person object will not be cast to the PersonEx type with help of as. The cast result will return null and the method returns false.

Thus, this is a correct behavior unlike with the previous one.

We can easily check it by executing the following code:

var person = new Person("John", "Smith", new DateTime(1990, 1, 1));
var personEx = new PersonEx("John", "Teddy", "Smith", new DateTime(1990, 1, 1));

bool isSamePerson = person.Equals(personEx);
bool isSamePerson2 = personEx.Equals(person);

However, we are interested in checking whether the implemented Equals(Object) behavior meets requirements of the documentation, rather than a logic correctness.

The requirement in question is as follows:

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

This string shows that it is not executed.

Also, we would like to check if the current implementation of Equals(Object) can cause any issues to the expected behavior.

Since a developer has no information how the objects will be compared – x.Equals(y) or y.Equals(x) – both in a client-side code and in hash-sets (hash-cards) or dictionaries (inside these sets/dictionaries), the program behavior will be non-deterministic and depend on implementation details.

Implementation of the Equals Method

Thus, we are going to consider how we can implement the Equals(Object) method to get the expected result.

Currently, it is possible to do it by following the method of Jeffrey Richter described in the book ‘CLR via C# (Part II «Designing Types», Chapter 5 «Primitive, Reference, and Value Types», Subchapter «Object Equality and Identity»)’: before comparing objects by value, we should determine whether the runtime types obtained using the Object.GetType () method (instead of one-way compatibility checks and cast object types with the operator ‘as’) are equal:

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

Note: This method is not unique as there are three ways to check the Type class instances for equality with different results for the identical operands:

  1. According to the documentation for the Object.GetType() method:

For two objects x and y that have identical runtime types, Object.ReferenceEquals(x.GetType(),y.GetType()) returns true.

Thus, it is possible to check objects of the Type class for equality comparing them by reference:

bool isSameType = (object)obj1.GetType() == (object)obj2.GetType();

or

bool isSameType = Object.ReferenceEquals(obj1.GetType(), obj2.GetType());
  1. The Type class contains the Equals(Object)and Equals(Type) methods whose behavior is determined in the following way:

Determines if the underlying system type of the current Type object is the same as the underlying system type of the specified Object.
Return Value
Type: System.Boolean
true if the underlying system type of o is the same as the underlying system type of the current Type; otherwise, false. This method also returns false if:
o is null.
o cannot be cast or converted to a Type object.

Remarks
This method overrides Object.Equals. It casts o to an object of type Type and calls the Type.Equals(Type) method.

And

Determines if the underlying system type of the current Type is the same as the underlying system type of the specified Type.

Return Value
Type: System.Boolean
true if the underlying system type of o is the same as the underlying system type of the current Type; otherwise, false.

The internal logic of these methods is as follows:

public override bool Equals(Object o)

{

if (o == null)

return false;

return Equals(o as Type); }

and

public virtual bool Equals(Type o)
{
    if ((object)o == null)
        return false;

    return (Object.ReferenceEquals(this.UnderlyingSystemType, o.UnderlyingSystemType));
}

As you can see, the result of these Equals methods for the Type class objects may differ from the comparison result of these objects by reference. The reason is that we do not compare Type objects but their  UnderlyingSystemType properties of the Type class.

However, it seems that Type.Equals(Object) is not designed to compare objects of the Type class.

  1. Starting from .NET Framework 4.0, the Type class provides overloaded operators == or !=

Indicates whether two Type objects are equal.

Return Value
Type: System.Boolean
true if left is equal to right; otherwise, false.

And

Indicates whether two Type objects are not equal.
Return Value
Type: System.Boolean
true if left is not equal to right; otherwise, false.

Source codes do not provide us with the information on implementation details to check an internal logic of operators:

public static extern bool operator ==(Type left, Type right);

public static extern bool operator !=(Type left, Type right);

Thus, we suppose that the most appropriate way to compare objects is using “==” and “!=” operators.

Depending on the target platform, it is possible to generate a source code using comparison by value or with == and != operators.

Implement the Person and PersonEx classes in the following manner:

using System;

namespace HelloEquatable
{
    public class Person
    {
        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() =>
            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 override bool Equals(object obj)
        {
            if ((object)this == obj)
                return true;

            if (obj == null)
                return false;

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

            return EqualsHelper(this, (Person)obj);
        }
    }
}
using System;

namespace HelloEquatable
{
    public class PersonEx : Person
    {
        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() =>
            base.GetHashCode() ^
            this.MiddleName.GetHashCode();

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

        public override bool Equals(object obj)
        {
            if ((object)this == obj)
                return true;

            if (obj == null)
                return false;

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

            return EqualsHelper(this, (PersonEx)obj);
        }
    }
}

Now, it is possible to meet the following requirement:

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

We can verify this by running the following code:

var person = new Person("John", "Smith", new DateTime(1990, 1, 1));
var personEx = new PersonEx("John", "Teddy", "Smith", new DateTime(1990, 1, 1));

bool isSamePerson = person.Equals(personEx);
bool isSamePerson2 = personEx.Equals(person);

Notes:

  1. Check for equality references to the current and specified objects. If they match, then it returns true.
  2. Check a reference to the specified object on a null. In the case of success, we get false.
  3. Check if the types of the current and specified objects are identical. If they are not equal, it is false.
  4. Cast the specified object to a type of the given class and compare objects by value.

Thus, we have found an optimal way to implement the expected behavior of the Equals(Object) method.

Finally, let’s check the correct implementation of Equals(Object) in a standard library.

The Uri.Equals(Object) method:

Compares two Uri instances for equality.

Syntax
public override bool Equals(object comparand)

Parameters
comparand
Type: System.Object
The Uri instance or a URI identifier to compare with the current instance.

Return Value
Type: System.Boolean
A Boolean value that is true if the two instances represent the same URI; otherwise, false.

public override bool Equals(object comparand)
{
    if ((object)comparand == null)
    {
        return false;
    }

    if ((object)this == (object)comparand)
    {
        return true;
    }

    Uri obj = comparand as Uri;

    //
    // we allow comparisons of Uri and String objects only. If a string
    // is passed, convert to Uri. This is inefficient, but allows us to
    // canonicalize the comparand, making comparison possible
    //
    if ((object)obj == null)
    {
        string s = comparand as string;

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

        if (!TryCreate(s, UriKind.RelativeOrAbsolute, out obj))
            return false;
    }

    // method code ...
}

Assume that the following requirement is not executed:

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

It occurs as the String class and the String.Equals(Object) method are not aware of the Uri class. We can verify this by running the following code:

const string uriString = "https://www.habrahabr.ru";
Uri uri = new Uri(uriString);

bool isSameUri = uri.Equals(uriString);
bool isSameUri2 = uriString.Equals(uri);

In my further articles, I will consider the implementation of the IEquatable(Of T) interface and the type-specific IEquatable(Of T).Equals(T) method, overload of equality and inequality operators to compare objects by value. In addition, I will find the most compact, consistent and performance way to implement all kinds of checks by value in one class.

Also read:

Comparing Objects by Value. Part 1: Beginning

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

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