Once again discussing equality I stumbled on EqualityComparer<T>.Default.Equals()
. I prefer to call this method for reference types rather than object.Equals()
.
Now I think I was dreadfully wrong.
object.Equals()
uses overridable instance Equals()
method providing correct polymorphic behavior whereas EqualityComparer<T>.Default.Equals()
calls IEquatable<T>.Equals()
if it's implemetned.
Now consider this small program:
public class Class1 : IEquatable<Class1>
{
public int Prop1 { get; set; }
public bool Equals(Class1 other)
{
if (other == null)
return false;
return Prop1 == other.Prop1;
}
public override bool Equals(object obj)
{
if (obj == null || GetType() != obj.GetType())
{
return false;
}
return Equals(obj as Class1);
}
}
public class Class2 : Class1, IEquatable<Class2>
{
public int Prop1 { get; set; }
public int Prop2 { get; set; }
public bool Equals(Class2 other)
{
if (other == null)
return false;
return Prop1 == other.Prop1 && Prop2 == other.Prop2;
}
public override bool Equals(object obj)
{
if (obj == null || GetType() != obj.GetType())
{
return false;
}
return Equals(obj as Class2);
}
}
class Program
{
static void Main(string[] args)
{
var c1 = new Class1 {Prop1 = 10};
var c2 = new Class2 {Prop1 = 10, Prop2 = 5};
var c3 = new Class2 {Prop1 = 10, Prop2 = 15};
Console.WriteLine("Object.Equals()");
Console.WriteLine("C1=C2 {0}",Equals(c1,c2));
Console.WriteLine("C2=C1 {0}",Equals(c2, c1));
Console.WriteLine("C2=C3 {0}",Equals(c2, c3));
Console.WriteLine("C3=C2 {0}", Equals(c3, c2));
var dec1 = EqualityComparer<Class1>.Default;
Console.WriteLine();
Console.WriteLine("EqualityComparer<Class1>.Default.Equals");
Console.WriteLine("C1=C2 {0}", dec1.Equals(c1, c2));
Console.WriteLine("C2=C1 {0}", dec1.Equals(c2, c1));
Console.WriteLine("C2=C3 {0} BUG?", dec1.Equals(c2, c3));
Console.WriteLine("C3=C2 {0} BUG?", dec1.Equals(c3, c2));
Console.ReadKey();
}
}
It shows how easy it is to bring inconsistency in equality semantics:
Object.Equals()
C1=C2 False
C2=C1 False
C2=C3 False
C3=C2 False
EqualityComparer<Class1>.Default.Equals
C1=C2 False
C2=C1 False
C2=C3 True BUG?
C3=C2 True BUG?
However MSDN Documentation recommdends:
Notes to Implementers If you implement Equals, you should also override the base class implementations of Object.Equals(Object) and GetHashCode so that their behavior is consistent with that of the IEquatable<T>.Equals method. If you do override Object.Equals(Object), your overridden implementation is also called in calls to the static Equals(System.Object, System.Object) method on your class. In addition, you should overload the op_Equality and op_Inequality operators. This ensures that all tests for equality return consistent results, which the example illustrates.
Starting from this moment I see no reason to implement IEquatable<T>
for reference types.
Can anyone tell me when it has any sense?
Should I really treat different equality behavior as inconsistent when we look at the type differently (as base type)?
Rightly or wrongly, here is how I have tended to implement Equals(Object)
and IEquatable<T>.Equals(T)
on base and derived classes.
public class Class1 : IEquatable<Class1>
{
public sealed override bool Equals(object obj)
{
return Equals(obj as Class1);
}
public virtual bool Equals(Class1 obj)
{
if(ReferenceEquals(obj, null))
return false;
// Some property checking
}
}
public class Class2 : Class1, IEquatable<Class2>
{
public sealed override bool Equals(Class1 obj)
{
return Equals(obj as Class2);
}
public virtual bool Equals(Class2 obj)
{
if(!base.Equals(obj))
return false;
// Some more property checking
}
}
public class Class3 : Class2, IEquatable<Class3>
{
public sealed override bool Equals(Class2 obj)
{
return Equals(obj as Class3);
}
public virtual bool Equals(Class3 obj)
{
if(!base.Equals(obj))
return false;
// Some more property checking
}
}
For reference types, the benefits of implementating IEquatable<T>
are marginal, if you have two instances of type T
, you are able to directly invoke T.Equals(T)
. instead of T.Equals(Object)
which subsequently requires type checking to be performed on the parameter.
The primary purpose of IEquatable<T>
is for value types, where there is overhead in boxing the instance.