Comparing objects in .NET can be a bit confusing. .NET gives us the object
base
class with its Equals()
method, then there are ==
and
!=
operators that can be overloaded and finally, we have interfaces like
IEquatable<>
, IComparable<>
and
IComparer<>
.
It also isn’t clearly stated whether these methods are supposed to compare object identity (check if two variables hold the same instance of a class) or object state (check if two variables carry objects with the same values).
This article tries to explain how .NET does comparison and demonstrates a good way to
implement the default Equals()
that elegantly works for inherited classes
and avoids duplicate code.
If You Do Nothing…
For classes (reference types), .NET will do any object identity comparison unless you
override the Equals()
method. The following code will output “false”:
class MyClass {
public int X;
public int Y;
}
class Program {
static void Main() {
MyClass first = new MyClass();
first.X = 12;
first.Y = 34;
MyClass second = new MyClass();
second.X = 12;
second.Y = 34;
// Will print false
Console.WriteLine(first.Equals(second));
}
}
For structures (value types), an object state comparison will happen instead. Thus, the same code as before with a structure will output “true”:
struct MyStruct {
public int X;
public int Y;
}
class Program {
static void Main() {
MyStruct first = new MyStruct();
first.X = 12;
first.Y = 34;
MyStruct second = new MyStruct();
second.X = 12;
second.Y = 34;
// Will print true
Console.WriteLine(first.Equals(second));
}
}
Because you can always do an object identity comparison using the static
object.ReferenceEquals()
method, comparison operators should default to
comparing an object’s state, not its identity.
In special cases where you actually need an identity comparison, you can do one of the following things, listed in order of preference:
- Explicitly use
object.ReferenceEquals()
when the comparison is in your own code - Use a special
IComparer<>
for collections or dictionaries - Let
Equals()
do an object identity comparison and document this behavior
Implementing object.Equals() in a Plain Class
My suggestion to you is to actually implement two Equals()
methods:
class MyClass {
public override bool Equals(object other) {
return Equals(other as MyClass);
}
public virtual bool Equals(MyClass other) {
if(ReferenceEquals(other, null))
return false;
return
(this.X == other.X) &&
(this.Y == other.Y);
}
public int X;
public int Y;
}
Let’s see: If someone compares an instance of MyClass
against an object of
a different type, the Equals(object other)
method is called, where the
as
operator will return null since the cast is not valid, effectively doing a
null
comparison which will always compare as false
(not equal).
The specialized Equals()
method provides a minor performance gain since we can
skip the up- and downcasting otherwise associated with the Equals()
method.
And since this
can never be null
for an instance method, we can
safely assume that if the comparison object is null
the comparison result is
false
.
If both objects are valid and of the same type, we proceed to compare the non-mutable fields of both instances, doing the actual object state comparison.
Implementing object.Equals() in a Derived Class
This scheme can be easily extended over multiple inheritance levels without repeating any code.
class MyDerivedClass : MyClass {
public override bool Equals(object other) {
return Equals(other as MyDerivedClass);
}
public override bool Equals(MyClass other) {
return Equals(other as MyDerivedClass);
}
public virtual bool Equals(MyDerivedClass other) {
return
base.Equals(other) &&
(this.Z == other.Z);
}
public int Z;
}
It’s a bit hard to see what happens when you compare MyDerivedClass
against
a null pointer, so let’s follow this code path:
MyDerivedClass.Equals(object)
is called- Which calls
MyDerivedClass.Equals(MyDerivedClass)
MyClass.Equals(MyClass)
is called-
MyClass.Equals(MyClass)
finds the comparison object isnull
and returns false
You will obtain one additional Equals()
overload for each level of inheritance
you create. I don’t see this as an issue because, as experience shows, well-designed
object models usually do not have more than 2 or 3 levels of inheritance.
The Equality Operators
By default, struct
s don’t have equality operators (==
and
!=
), so you can’t use these operators on a struct unless it explicitly
defines these operators. For classes, the default equality operators do an identity
comparison, i.e. will return true
if both sides of the comparison refer
to the same object and false
in any other case.
These can make use of the specialized Equals()
operator and are therefore easy to implement:
/// <summary>Checks two segment instances for inequality</summary>
/// <param name="first">First instance to be compared</param>
/// <param name="second">Second instance fo tbe compared</param>
/// <returns>True if the instances differ or exactly one reference is set to null</returns>
public static bool operator !=(MyClass first, MyClass second) {
return !(first == second);
}
/// <summary>Checks two segment instances for equality</summary>
/// <param name="first">First instance to be compared</param>
/// <param name="second">Second instance fo tbe compared</param>
/// <returns>True if both instances are equal or both references are null</returns>
public static bool operator ==(MyClass first, MyClass second) {
if(ReferenceEquals(first, null))
return ReferenceEquals(second, null);
return first.Equals(second);
}
As you can see, the entire logic has been moved to the
==
operator to avoid duplicating code lines.
Of course, since the comparison operators are static methods,
we now might encounter the case where the left operand is
null
. This is handled by checking whether the
left operand is null
and then only returning
true
if the right operand is also null
(so null == null
would evaluate as true
).
If only the right operand is null
, this will be
handled by the Equals()
operator.
Drawbacks
The only drawback of this implementation is that you have to
implement the specialized Equals()
operator in all
derived classes and obtain one more Equals()
specialization for each level of inheritance. You might want
to accept the performance loss of the up- and redowncast
dilemma in some special cases and only implement an
Equals(object other)
operator there.