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:
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:
You could then easily override equals with this definition (using C# 7 pattern matching syntax):
This works well until someone creates
Point3D as a subclass:
You might be able to predict what happens next …
Why did this go wrong? The
is Point2D test in the implementation of
.Equals(object) returns true even when given an instance of
One way to avoid this is to explicitly check that the types of the two classes match:
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.
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:
It might be tempting to extend this to allow testing for equality with a string:
The problem with this is the lack of symmetry. Consider this example:
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.
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.