Written by 15:55 Computer Environment, Languages & Coding

Comparing Objects by Value. Part 5: Structure Equality Issue

In my previous publication, I described the fullest and the most correct way to compare objects by value – class instances that represent reference types in the .NET framework.

Now, I am going to determine how it is possible to modify the proposed method to compare instances of the structs that represent value types.

It should be noted that it is possible to compare instances of the structs by value.

For the predefined types such as Boolean and Int32, comparison by value means comparing field values of structs instances. The same is applied for a user defined struct. For more information, see the ValueType.Equals(Object) method and the == and != operators.

In addition, the ValueType.GetHashCode() method that overrides the Object.GetHashCode() method will be implemented as well.

In this case, we should keep in mind the following points:

  1. To compare field values, it is necessary to use reflection that impacts performance.
  2. The struct field may have a reference type, the comparison of fields may be implemented by value, rather than by reference. Though, I would like to add that using reference types in the structure may be considered as a wrong architecture solution.
    The documentation states that we should create a custom implementation of comparing by value to improve performance and return the most accurate value for equality.
  1. It may seem that we should not consider all the fields to be compared. Still, this solution may be wrong.
  2. The default implementation of the GetHashCode() method does not meet general requirements to the GetHashCode() method implementation:
  • The return value may be unsuitable as a key in a hash table;
  • If a value of any object fields has been modified, then the return value may become unsuitable for use as a key in a hash table;
  • Also, we need to create a custom implementation of the GetHashCode() method that most represents the concept of a hash code for the type.

Thus, on the one hand, there are several reasons for implementing a custom mechanism of comparing objects by value, namely performance, and domain model match.

On the other hand, the need to automatically implement the GetHashCode() method leads to the need to implement a comparison by value (see the first publication). It should be done as the GetHashCode() method must be aware what data and how it can be compared by value.

In addition, the following case may be possible: when there is a simple structure containing only structure fields which returns a semantically correct result when comparing byte-by-byte with reflection.

In this case, it is quite possible to implement GetHashCode() in a correct way to return the same hash code for equal objects without creating a custom implementation of comparing by value.

For example:

 public struct Point
    {
        private int x;

        private int y;

        public int X {
            get { return x; }
            set { x = value; }
        }

        public int Y
        {
            get { return y; }
            set { y = value; }
        }

        public override int GetHashCode() => x.GetHashCode() ^ y.GetHashCode();
    }

However, when writing this example with Auto-Implemented properties, we have the following:

public struct Point
    {
        public int X { get; set; }

        public int Y { get; set; }

        public override int GetHashCode() => X.GetHashCode() ^ Y.GetHashCode();
    }

According to the documentation, the compiler creates a private, anonymous backing field that meets the public properties.

Strictly speaking, it is not clear whether two Point objects with pairwise identical values of X and Y will be equal when implementing the default comparison by value. Thus, this triggers the following questions:

  • If the default implementation compares values of fields by reflection, then how can we match anonymous fields (X and Y) for different objects?
  • If two different objects have backing fields with different names such as (x1, y1) and (x2, y2), should we take into account that x1 matches x2, and y1 – y2?
  • Whether is it possible to create any additional fields that may have different values for identical objects (X and Y)? If so, will they be used in the comparison?
  • Or, perhaps, when we have a structure with auto-implemented properties, will a byte-by-byte comparison of all the content be used without creating separate fields? If so, will the backing fields for each object always be created in the same order and with the same offsets?

Most likely, you will find positive answers to these questions in the documentation, meaning that behavior for structures with explicitly declared fields and structures with auto-implemented properties will return the same result in the comparison by value.

Alternatively, it may turn out that we will get the expected behavior of a compiler and runtime.

Therefore, taking into account all the points, we assume that it is better to implement a custom comparison by value.

In future, I am going to write an article with detailed comments on a particular example with the Person entity.

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 6: Structure Equality Implementation

Tags: , Last modified: September 23, 2021
Close