If we’re going to implement equality correctly, we need to consider the contract we’re implementing - what are the characteristics of a proper implementation of equality? The first characteristic we need to consider is symmetry.

When testing for equality of two instances, the order of instances shouldn’t matter - you should get the same answer either way around. As code:

alpha.Equals(beta) == beta.Equals(alpha);

Inheritance

For simple types, it’s easy to get this right - but when object inheritance comes into play, things can get more complicated.

Imagine you have a class for a two-dimensional point:

public class Point2D
{
    public double X { get; }
    public double Y { get; }
}

You could then easily override equals with this definition (using C# 7 pattern matching syntax):

public override Equals(object instance) 
    => instance is Point2D p
        && p.X == X
        && p.Y == Y;

This works well until someone creates Point3D as a subclass:

public class Point3D: Point2D
{
    public double Z { get; }
}

You might be able to predict what happens next …

var alpha = new Point2D( 3, 4 );
var beta = new Point3D( 3, 4, 5);
var first = alpha.Equals(beta); // true (!!)
var second = beta.Equals(alpha); // false (correct)

Why did this go wrong? The is Point2D test in the implementation of .Equals(object) returns true even when given an instance of Point3D.

One way to avoid this is to explicitly check that the types of the two classes match:

public override Equals(object instance) 
    => instance is Point2D p
        && GetType() == p.GetType() // Added
        && p.X == X
        && p.Y == Y;

Another approach would be to declare Point2D as a struct; this would then force Point3D to be independently implemented as a different struct. This would likely be a good idea, as inheritance is almost certainly the wrong way to solve that problem.

In some codebases, they only implement .Equals(object) in sealed leaf classes and make it abstract elsewhere else. I’ve seen this used to good effect in some places, though in others it feels like an excessive amount of ceremony.

Type compatibility

Another way that the symmetry of equality can trip us up is when we start doing comparisons with other types.

Imagine you defined a semantic type to represent a time series identifer:

public class TimeSeriesId
{
    private readonly string _id;
    public TimeSeriesId(string id) => _id = id;
    public override bool Equals(object obj) => ...
}

It might be tempting to extend this to allow testing for equality with a string:

public class TimeSeriesId : IEquatable<string>
{
    private readonly string _id;
    public TimeSeriesId(string id) => _id = id;
    public bool Equals(string id) => ... // new
    public override bool Equals(object obj) => ...
}

The problem with this is the lack of symmetry. Consider this example:

var id = "MON.MMC";
var tid = new TimeSeriesId(id);

// Direct comparison
tid.Equals(id); // returns true
id.Equals(tid); // returns false

// Using the static object.Equals()
object.Equals(tid, id); // returns true;
object.Equals(id, tid); // returns false;

Our TimeSeriesId knows how equality with a string is supposed to work, but string does not - and of course, it can’t because string is a global type that everyone uses and TimeSeriesId is a local type only known to us.

While you can control the order in your own code, you can’t control it in any other code - in third party libraries and in the framework itself, you have no control over which reference is used first and which is used second.

Conclusions

Our implementations of .Equals() have to be symmetric. Failure to adhere to this aspect of the equality contract will lead to bugs in our software.

About this series

Why does the implementation of Equality matter in .NET and how do you do it right?

Posts in this series

Why is Equality important in .NET?
Types of Equality
Equality has Symmetry
Equality and GetHashCode
Implementing Entity Equality
Implementing Value Equality
Types behaving badly
Prior post in this series:
Types of Equality
Next post in this series:
Equality and GetHashCode

Comments

blog comments powered by Disqus