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:

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.

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:

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:

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:

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:

or

  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:

and

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:

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:

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:

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.

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:

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.

Andrey Sergeev

Andrey Sergeev

Andrey Sergeev is experienced DDD, C# and .NET developer, and is interested in Java and Ruby stacks.
For last ten years, Andrey developed the multi-tier traceability system and business analysis systems.
In present, Andrey is developing the famous mobile fitness application.
Andrey Sergeev

Andrey Sergeev

Andrey Sergeev is experienced DDD, C# and .NET developer, and is interested in Java and Ruby stacks. For last ten years, Andrey developed the multi-tier traceability system and business analysis systems. In present, Andrey is developing the famous mobile fitness application.