The .NET framework gives us the object base class
and the Equals() operator. Then there are the
== and the != operators as language
features of C# and other managed languages.
It also isn't clearly stated whether Equals() is
supposed to compare object identity (check if two objects
are the exact same instance) or object state (check if two
objects carry the same values) and unless you explicitely
implement the Equals() method, an object identity
comparison is done.
Since you can always do an object identity comparison by using
the ReferenceEquals() method, you should always
write your comparison operators so they do an object state
comparison. In the few special cases where you really need
object identity comparison, you can either create an adapter
class or explicitely state this behavior in the documentation
for your Equals() operator.
The Equals() Method
My suggestion to you is to actually implement two
Equals() methods:
/// <param name="other">Other instance to compare to this instance</param>
/// <returns>True if the other instance is equal to this instance</returns>
public override bool Equals(object other) {
return Equals(other as MyClass);
}
/// <summary>Checks whether another instance is equal to this instance</summary>
/// <param name="other">Other instance to compare to this instance</param>
/// <returns>True if the other instance is equal to this instance</returns>
public virtual bool Equals(MyClass other) {
if(ReferenceEquals(other, null))
return false;
return (this.Start == other.Start) && (this.End == other.End);
}
Let's see: If someone compares an instance of
MyClass against an object of another 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.
The Comparison Operators
These can make use of the specialized Equals()
operator and are therefore easy to implement:
/// <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.
Post new comment