Written by 17:55 Computer Environment, Languages & Coding

Comparing Objects by Value. Part 1. Beginning

It is a common fact that the .NET object model, as well as other software program platforms, allow comparing objects by reference and by value.

By default, two objects are equal if the corresponding object variables have the same reference. Otherwise, they are different.

However, in some cases, you may need to state that two objects belonging to the same class are equal if their content match in a certain way.

Assume we have the Person class, which contains some personal data – First Name, Last Name, and Birth date.

Consider the following points:

  1. What is the minimum required number of class modifications to assure comparing class objects by values with the help of the standard .NET architecture?
  2. What is the minimum required number of class modifications to assure comparing class objects by values (every time, if not explicitly stated that objects may be compared by a reference) with the help of the standard .NET architecture?

For each case, we will see the best way to compare objects by value to get a consistent, compact, copy-paste free, and productive code. It is not as trivial as it may seem for the first time.

In addition, we will see what modifications may be added to the platform to simplify the task implementation.

The Person class:

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

We have a great choice of methods to compare objects:

Objects are equal if object references are equal. The same rule applies for hash sets and dictionaries.

To compare objects by value in a client-side code it is necessary to write the following type strings:

var p1 = new Person("John", "Smith", new DateTime(1990, 1, 1));
var p2 = new Person("John", "Smith", new DateTime(1990, 1, 1));

bool isSamePerson =
    p1.BirthDate == p2.BirthDate &&
    p1.FirstName == p2.FirstName &&
    p1.LastName == p2.LastName;

Notes:

  1. For the Person class, the FirstName and LastName are not null.
    In case, FirstName and LastName are unknown, we will use String.Empty, which allows us to avoid NullReferenceException while accessing FirstName and LastName. Also, this allows avoiding collisions when comparing NULLs and String.Empty.
  1. The BirthDate property is implemented as a Nullable(Of T)-structure. Thus, if the birthdate is not specified or unknown, it is better to save an undefined value rather than a value like 01/01/1900, 01/01/1970, 01/01/0001 or MinValue.
  2. While comparing objects by value, the date comparison goes first, as it takes less time to compare the Date/Time variables, than a string comparison.
  3. Date and string comparison are implemented using an equality operator, as it compares structures and strings by value.

To use the following methods for the Person class object comparison:

You need to override Object.Equals(Object) and Object.GetHashCode() methods in the following way:

  • The Equals(Object) method compares those fields of a class whose value combination determines an object value.
  • The GetHashCode() method returns the same values of hash-codes for equal objects, namely Equals(Object) returns true.
    Thus, if objects have different hash-codes, then they are not equal (though unequal objects may have the same hash-codes).
    (To get a hash-code, we should use the result of ‘exclusive OR’ for GetHashCode() values used in Equals to compare objects by value.
    If a field is a 32-bit integer, it is possible to use the field value instead of the hash-code.

When two unequal objects have the same hash-code, you may minimize collision by using different optimizations).

I would like to draw your attention that according to the documentation the Equals(Object) method can be used when it meets the following requirements:

  • Equals(y) returns the same value as y.Equals(x).
  • If (x.Equals(y) && y.Equals(z)) returns true, then x.Equals(z) returns true.
  • Equals(null) returns false.
  • Successive calls to x.Equals(y) return the same value as long as the objects referenced by x and y are not modified.
  • Other requirements that refer to the rules of comparing values with a floating point.

Also, we should keep in mind that according to the documentation as the value we receive using the GetHashCode() method is not constant, we should neither store it to our disk nor to a database nor use it as a key or as a way to compare objects.

The Person class with overridden methods Equals(Object) and GetHashCode():

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;

            var other = obj as Person;

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

            return EqualsHelper(this, other);
        }
    }
}

Notes:

  • If any field contains null, then we can use “zero-value” instead of the GetHashCode() value.
  • The FirstName and LastName reference fields cannot contain null. The BirthDate field is the Nullable(Of T)-structure. If GetHashCode() is null, it returns zero, and there is no NullReferenceException when calling GetHashCode().
  • If the Person class could contain null, then the GetHashCode() method would have the following structure:
public override int GetHashCode() =>
    this.FirstName?.GetHashCode() ?? 0 ^
    this.LastName?.GetHashCode() ?? 0 ^
    this.BirthDate?.GetHashCode() ?? 0;

Consider how the Equals(Object) method is implemented:

  1. The reference to the current object (THIS) is compared to the reference on the specified object. If references are equal, it is true.
  2. The next step is the casting of the specified object to the type of Person by using the as If the result equals null, then it is false (The specified reference has been originally equal to null, or the specified object has a type that is incompatible with the Person class. Thus, it is not equal to the current object).
  3. Then, two objects of the Person class are compared by value.

To improve the code readability and allow code reusing, we have created EqualsHelper.

So far, we have implemented only a part of required functionality to compare objects by value. Still, we have some questions.

The first one is theoretical.

Consider the requirement of the Equals(Object):

x.Equals(null) returns false.

I wonder why some instance methods in a standard .NET library check this to null – for example, the String.Equals(Object) method does this:

public override bool Equals(Object obj) {
    //this is necessary to guard against reverse-pinvokes and
    //other callers who do not use the callvirt instruction
    if (this == null)
        throw new NullReferenceException();

    String str = obj as String;
    if (str == null)
        return false;

    if (Object.ReferenceEquals(this, obj))
        return true;

    if (this.Length != str.Length)
        return false;

     return EqualsHelper(this, str);
}

This method checks this to null. If it is true, then NullReferenceException is thrown.

The comment specifies when this equals null.

To compare this to null, I have used a == operator which is overloaded in the String class. Thus, it would be better to cast this to object explicitly: (object)this == null. Alternatively, you may use the Object.ReferenceEquals(Object, Object) method.

Read this article to get detailed information on this topic: When this == null: a True Story of the CLR World.

However, in this case, if we call the overloaded method Person.Equals(Object) without creating an instance and pass null as an input parameter then (if ((object)this == obj) return true;) that does not meet method requirements.

In addition, the documentation does not specify, should we check this to null and throw an exception if this check is successful.

Thus, I suggest additional requirements to the Equals(Object) method which is as follows:

  • If references to the current and specified objects are equal, then true is returned (for classes);
  • If a reference to the specified object is null, then false is returned.

The second question regarding the Equals(Object) method implementation is more interesting than the previous one and has an applied meaning.

It refers to the correct implementation of the following requirement:

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

We have analyzed whether all the requirements and the examples of the method implementation are specified in the documentation and if there are any alternative ways to implement this method. We will talk about the previous question as well as a full set of class modifications to compare its objects by value in the following publications.

Also read:

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 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