Written by 13:16 Database administration, Database Optimization & Structure

Comparing Objects by Value. Part 6: Structure Equality Implementation

We have already analyzed peculiarities of structs of the .NET framework that represent Value Types when comparing objects by value – instance of structs.

Now, I am going to describe this process on a particular example to check whether it will allow us to determine the use of the object comparison by value in general and thus, to simplify a sample of comparing objects by value – class instances that represent reference types.

The PersonStruct structure:

using System;

namespace HelloEquatable
{
    public struct PersonStruct : IEquatable<PersonStruct>, IEquatable<PersonStruct?>
    {
        private static int GetHashCodeHelper(int[] subCodes)
        {
            int result = subCodes[0];

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

            return result;
        }

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

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

        public string FirstName { get; }

        public string LastName { get; }

        public DateTime? BirthDate { get; }

        public PersonStruct(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()
            }
        );

        public static bool Equals(PersonStruct first, PersonStruct second) =>
            first.BirthDate == second.BirthDate &&
            first.FirstName == second.FirstName &&
            first.LastName == second.LastName;

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

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

        public bool Equals(PersonStruct other) =>
            Equals(this, other);

        public static bool Equals(PersonStruct? first, PersonStruct? second) =>
            first == second;
        // Alternate version:
        //public static bool Equals(PersonStruct? first, PersonStruct? second) =>
        //    first.HasValue == second.HasValue &&
        //    (
        //        !first.HasValue || Equals(first.Value, second.Value)
        //    );

        public bool Equals(PersonStruct? other) => this == other;
        // Alternate version:
        //public bool Equals(PersonStruct? other) =>
        //    other.HasValue && Equals(this, other.Value);

        public override bool Equals(object obj) =>
            (obj is PersonStruct) && Equals(this, (PersonStruct)obj);
        // Alternate version:
        //public override bool Equals(object obj) =>
        //    obj != null &&
        //    this.GetType() == obj.GetType() &&
        //    Equals(this, (PersonStruct)obj);
    }
}

As you can see, this example is smaller and easier by structure, as instances of structs are not null and it is not possible to inherit from user-defined structs. We have already discussed peculiarities to implement the comparison by value for the class instances in my previous article.

In addition, we have determined fields for object comparison as well as implemented the GetHashCode() method.

Methods and operators of comparison have been implemented in the following order:

  1. To compare two instances of structs, we have implemented the PersonStruct.Equals(PersonStruct, PersonStruct) static method. We will use this method as a reference comparison method when implementing other methods and operators. Additionally, it can be applied to compare instances of structs in languages that do not support operators.
  2. The PersonStruct.==(PersonStruct, PersonStruct) and PersonStruct.!=(PersonStruct, PersonStruct) operators have been implemented as well. It should be noted that a C# compiler has the following peculiarities:
  • You may compare with the overloaded operators T.==(T, T) and T.!=(T, T) in the Nullable(Of T)
  • Before checking a value equality, a compiler may verify whether instances of structs have a valid value. In addition, a compiler does not wrap instances of structs into objects.
  • Thus, comparing instances of the Nullable(Of T)structure with an untyped nullable value leads to calling the ==(T, T) or T.!=(T, T) operators, while comparing instances of the Nullable(Of T) structure without overloaded operators T.==(T, T)and T.!=(T, T) results in calling the Object.==(Object, Object) or Object.!=(Object, Object) operators and, as a result, in wrapping an instance into the object.
  1. The PersonStruct.Equals(PersonStruct) method (implementation of IEquatable(Of PersonStruct)) has been implemented by calling the PersonStruct.Equals(PersonStruct, PersonStruct) method.
  2. To avoid wrapping instances of structs into objects, when we have one or two Nullable(Of PersonStruct) instances, it is possible to implement the following methods:
  • PersonStruct.Equals(PersonStruct?, PersonStruct?), as a call of the PersonStruct.==(PersonStruct, PersonStruct) operator, is used to avoid wrapping instances of structs of both arguments into objects and calling the Object.Equals(Object, Object) method if at least one of the arguments is a Nullable(Of PersonStruct) instance. Additionally, you may use this method to compare Nullable(Of PersonStruct) instances in languages that do not support operators. In the code, you may find comments explaining how this method could be implemented if a C# compiler was not able to use the T.==(T, T) and T.!=(T, T) operators for the Nullable(Of T) arguments.
  • PersonStruct.Equals(PersonStruct?) – the implementation of the IEquatable(Of PersonStruct?) interface used to avoid wrapping the Nullable(Of PersonStruct) arguments into objects and calling the PersonStruct.Equals(Object) method. It is implemented as a call of the PersonStruct.==(PersonStruct, PersonStruct) operator with the commented code to use T.==(T, T) and T.!=(T, T) operators for the Nullable(Of T) arguments.
  • PersonStruct.Equals(Object) – that overrides the Object.Equals(Object) method. It is implemented by checking the compatibility of an argument type with a type of the current object using the is operator by casting the argument to PersonStruct and calling PersonStruct.Equals(PersonStruct, PersonStruct).

Notes:

  • The implementation of the IEquatable(Of PersonStruct?) — IEquatable(Of Nullable(Of PersonStruct))interface serves to show particular issues in the platform when working with structs where wrapping instances into objects happens faster than we expect.
  • In real projects, provided that it is not necessary to improve performance, the implementation of IEquatable(Of Nullable(Of T)) is not applicable for architecture reasons – we should not implement typed IEquatable in the T type for any type.
  • In general, it is not necessary to overwhelm a code with different optimizations.

For structures, we could achieve to make the comparison by value to be much simpler and more productive by avoiding inheritance of User defined structs and need to check objects on null. In addition, we can monitor a new logic that supports Nullable(Of T) arguments.

In my future publication, I will summarize the following points:

  • When it is a good idea to implement comparison of objects by value;
  • How we may simplify the implementation of comparison by value for objects – class instances that represent reference types.

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 4: Inheritance & Comparison Operators

Comparing Objects by Value. Part 5: Structure Equality Issue

Tags: , Last modified: September 23, 2021
Close