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:
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:
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 Point3D
.
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.
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:
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:
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.
Comments
blog comments powered by Disqus