With two immutable classes Base and Derived (which derives from Base) I want to define Equality so that
equality is always polymorphic - that is
((Base)derived1).Equals((Base)derived2)
will callDerived.Equals
operators
==
and!=
will callEquals
rather thanReferenceEquals
(value equality)
What I did:
class Base: IEquatable<Base> {
public readonly ImmutableType1 X;
readonly ImmutableType2 Y;
public Base(ImmutableType1 X, ImmutableType2 Y) {
this.X = X;
this.Y = Y;
}
public override bool Equals(object obj) {
if (object.ReferenceEquals(this, obj)) return true;
if (obj is null || obj.GetType()!=this.GetType()) return false;
return obj is Base o
&& X.Equals(o.X) && Y.Equals(o.Y);
}
public override int GetHashCode() => HashCode.Combine(X, Y);
// boilerplate
public bool Equals(Base o) => object.Equals(this, o);
public static bool operator ==(Base o1, Base o2) => object.Equals(o1, o2);
public static bool operator !=(Base o1, Base o2) => !object.Equals(o1, o2); }
Here everything ends up in Equals(object)
which is always polymorphic so both targets are achieved.
I then derive like this:
class Derived : Base, IEquatable<Derived> {
public readonly ImmutableType3 Z;
readonly ImmutableType4 K;
public Derived(ImmutableType1 X, ImmutableType2 Y, ImmutableType3 Z, ImmutableType4 K) : base(X, Y) {
this.Z = Z;
this.K = K;
}
public override bool Equals(object obj) {
if (object.ReferenceEquals(this, obj)) return true;
if (obj is null || obj.GetType()!=this.GetType()) return false;
return obj is Derived o
&& base.Equals(obj) /* ! */
&& Z.Equals(o.Z) && K.Equals(o.K);
}
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), Z, K);
// boilerplate
public bool Equals(Derived o) => object.Equals(this, o);
}
Which is basically the same except for one gotcha - when calling base.Equals
I call base.Equals(object)
and not base.Equals(Derived)
(which will cause an endless recursion).
Also Equals(C)
will in this implementation do some boxing/unboxing but that is worth it for me.
My questions are -
First is this correct ? my (testing) seems to suggest it is but with C# being so difficult in equality I'm just not sure anymore .. are there any cases where this is wrong ?
and Second - is this good ? are there better cleaner ways to achieve this ?
Well I guess there are two parts to you problem:
Would this work? https://dotnetfiddle.net/eVLiMZ (I had to use some older syntax as it didn't compile in dotnetfiddle otherwise)
The code can be simplified using a combination of an extension method and some boilercode. This takes almost all of the pain away and leaves classes focused on comparing their instances without having to deal with all the special edge cases:
I can now do:
(testing)
For immutable classes it is possible to optimize further by caching the hashcode and using it in Equals to shortcut equality if the hashcodes are different:
I can now do:
Which is not too bad - there is more complexity but it is all just boilerplate which I just cut&paste .. the logic is clearly separated in
ThisEquals
andThisHashCode
(testing)
Another method would be to use Reflection to automatically compare all of your fields and properties. You just have to decorate them with the
Immutable
attribute andAutoCompare()
will take care of the rest.This will also use Reflection to build a HashCode based on your fields and properties decorated with
Immutable
, and then cache it to optimize the object comparison.This method of comparison using Reflection which, other than the extension methods, is simpler. It also keeps private members private.
All of the logic is in the
IImmutableExtensions
class. It simply looks at what fields are readonly and uses them for the comparison.You don't need methods in the base or derived classes for the object comparison. Just call the extension method
ImmutableEquals
when you are overriding==
,!=
, andEquals()
. Same with the hashcode.And the
IImmutableExtensions
class:Old answer: (I will leave this for reference)
Based on what you were saying about having to cast to
object
it occurred to me that the methodsEquals(object)
andEquals(Base)
were too ambiguous when calling them from a derived class.This said to me that the logic should be moved out of both of the classes, to a method that would better describe our intentions.
Equality will remain polymorphic as
ImmutableEquals
in the base class will call the overriddenValuesEqual
. This is where you can decide in each derived class how to compare equality.This is your code refactored with that goal.
Revised answer:
It occurred to me that all of our logic in
IsEqual()
andGetHashCode()
would work if we simply supplied a tuple that contained the immutable fields that we wanted to compare. This avoids duplicating so much code in every class.It is up to the developer that creates the derived class to override
GetImmutableTuple()
. Without using reflection (see other answer), I feel this is the least of all evils.