structural equality in C#
5
(1)

In a previous article about equality in C# I talked about different equality types and then discussed the reference equality focusing on the inconsistent behavior you can get if you’re not checking correctly for reference equality. In this article I’m going one step forward and dive into the more complex structural equality, focusing on how you can implement it in your own C# classes and structs. Please note that any discussion around structural equality in record types is not in scope for this post.

When To Check for Structural Equality?

When you define a class or struct, you decide whether it makes sense to create a custom definition of structural equality (or equivalence) for the type. Remember from the previous article that the default Equals() method on the object class checks for reference equality. This means that in your classes you already have a default definition of equality. However, if you want to add custom types (for now I think about classes) to a collection and prevent value duplication you might need to implement your definition of equality. Or, simply put, you should ask yourself when are two “apples” equal?

Further, also please recall from the previous article that in case of structs (which are value types) the Equals() method is overridden to check for value equality instead of reference equality. So why would you want to override the definition of equality in your structs?

Well, the default implementation of structural equality in structs uses reflection to examine all the fields and properties in the type. This means that the equality check will always produce correct results but it is very slow in comparison to a custom definition of value equality. In a very simplistic perspective the slower performance is due to the fact that the CLR doesn’t know beforehand what your properties are so it needs to use reflection to loop through all the struct properties. In a custom equality definition the developer declares that  “Name” and “Age” properties for instance should match when two different struct objects are compared for equality. However, the full and detailed explanation is more complex and you can read it in this article.

Long story short: you should implement your custom definition of value equality both in classes (because the default implementation checks for reference equality) and in structs (because the default implementation of value equality doesn’t excel when it comes to performance)!

The 3 Principles of Structural Equality

Implementing structural equality seems to be very simple. And it is indeed! However, implementing it the right way requires that we adhere to three very important principles:

  1. Equality should be reflexivex.Equals(x). Seems logical enough. An object should always be equal to itself.
  2. Equality should be symmetricx.Equals(y) returns the same value as y.Equals(x)
  3. Equality should be transitiveif (x.Equals(y) && y.Equals(z)) returns true, then x.Equals(z) returns true

I know that the official C# documentation outlines five guarantees of equality but in my opinion the last two principles mentioned by the documentation will already be respected if these three are respected.

With this three principles implementing  structural equality in C# seems to bring more challenges than we would expect! Fair enough! However, there are a few basic things to consider when implementing a custom equality definition. If you follow the steps below, your custom definition of equality will adhere to the mentioned principles.

To make sure you implement structural equality in your types the right way, take a note of the following steps:

  1. Implement the IEquatable<T> interface
  2. Override the GetHashCode() method
  3. Override the == and != operators

Let’s go a little bit deeper into all these steps.

Implementing the IEquatable<T> Interface

The IEquatable<T> interface defines a generalized method that a value type or class implements to create a type-specific method for determining equality of instances. The contract of this interface is fairly straightforward: you just need to implement the Equals() method defined in the interface. However, if you implement this interface you also need to override the Equals() method on the object class. Otherwise the behavior of your type won’t be consistent since your custom equality definition will check for structural equality while the object Equals() method on the object class will check for reference equality. Here’s an example:

static void Main(string[] args)
{
    var p1 = new Point(3, 5);
    var p2 = new Point(3, 5);

    //returns True
    Console.WriteLine($"Calling the interface implementation: {p1.Equals(p2)}");

    //returns False
    //if you override object.Equals() both will return true
    Console.WriteLine($"Calling the Equals method on the base class: {object.Equals(p1, p2)}");


}

Therefore, your implementation should look similar to the following code snippet:

public override bool Equals(object obj)
{
    return Equals(obj as Point);
}

public bool Equals(Point p)
{
    // If parameter is null, return false.
    if (Object.ReferenceEquals(p, null))
    {
        return false;
    }

    // Optimization for a common success case.
    if (Object.ReferenceEquals(this, p))
    {
        return true;
    }

    // If run-time types are not exactly the same, return false.
    if (GetType() != p.GetType())
    {
        return false;
    }

    // Return true if the fields match.
    return (X == p.X) && (Y == p.Y);
}

Override the GetHashCode() Method

The GetHashCode() method in C# is used to compute a hash for each object. While hash collision is theoretically possible the object hash code is often used in reference to equality in C#. This means that there is a tight bond between your structural equality definition and the hash code of each object.

When overriding the GetHashCode() method, the implementation should reflect your definition of equality. This means that the hash code should be computed using a pattern that includes all properties that you used in your equality definition. While there are countless discussions on what is the best pattern to use when generating the hash code for an object, here is a very basic and possible example:

public override int GetHashCode()
{
    return X.GetHashCode() * 17 + Y.GetHashCode();
}

Override the Equality Operators

This step is optional but it’s a nice to have if you’ve gone this far with your type. Here’s a very basic example on how these overrides could look like:

public static bool operator ==(Point lhs, Point rhs)
{
    // Check for null on left side.
    if (Object.ReferenceEquals(lhs, null))
    {
        if (Object.ReferenceEquals(rhs, null))
        {
            // null == null = true.
            return true;
        }

        // Only the left side is null.
        return false;
    }

    // Equals handles case of null on right side.
    return lhs.Equals(rhs);
}

public static bool operator !=(Point lhs, Point rhs)
{
    return !(lhs == rhs);
}

That’s mostly it. The bottom line is to understand that for a correct structural equality check it is not enough to just implement the IEquatable<T> interface. If you do this, your code will show inconsistent behavior that might generate some nasty bugs down the line.

P.S: If you enjoyed this article, you might also want to check my YouTube channel for a lot more and visual .NET content. I also run a newsletter that’s different to any other newsletter as it focuses more on things that will help you become a better engineer, not just a better coder. You might want to subscribe to it as well!

How useful was this post?

Click on a star to rate it!

Average rating 5 / 5. Vote count: 1

No votes so far! Be the first to rate this post.

We are sorry that this post was not useful for you!

Let us improve this post!

Tell us how we can improve this post?

By Dan Patrascu

I solve business problems through quality code, smart architecture, clever design and cloud sorcery. Over more than 8 years I've worked and led projects of all shapes and sizes: from regular monoliths to complex microservices and everything in between. I'm also running the Codewrinkles channel and a newsletter.