Equality and polymorphism
c#, c#-7.0, c#-7.3
Solution
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:
namespace System {
public static partial class ExtensionMethods {
public static bool Equals<T>(this T inst, object obj, Func<T, bool> thisEquals) where T : IEquatable<T> =>
object.ReferenceEquals(inst, obj) // same reference -> equal
|| !(obj is null) // this is not null but obj is -> not equal
&& obj.GetType() == inst.GetType() // obj is more derived than this -> not equal
&& obj is T o // obj cannot be cast to this type -> not equal
&& thisEquals(o);
}
}
I can now do:
class Base : IEquatable<Base> {
public SomeType1 X;
SomeType2 Y;
public Base(SomeType1 X, SomeType2 Y) => (this.X, this.Y) = (X, Y);
public bool ThisEquals(Base o) => (X, Y) == (o.X, o.Y);
// boilerplate
public override bool Equals(object obj) => this.Equals(obj, ThisEquals);
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);
}
class Derived : Base, IEquatable<Derived> {
public SomeType3 Z;
SomeType4 K;
public Derived(SomeType1 X, SomeType2 Y, SomeType3 Z, SomeType4 K) : base(X, Y) => (this.Z, this.K) = (Z, K);
public bool ThisEquals(Derived o) => base.ThisEquals(o) && (Z, K) == (o.Z, o.K);
// boilerplate
public override bool Equals(object obj) => this.Equals(obj, ThisEquals);
public bool Equals(Derived o) => object.Equals(this, o);
}
This is good, no casting or null checks and all the real work is clearly separated in `ThisEquals`. (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:
namespace System.Immutable {
public interface IImmutableEquatable<T> : IEquatable<T> { };
public static partial class ExtensionMethods {
public static bool ImmutableEquals<T>(this T inst, object obj, Func<T, bool> thisEquals) where T : IImmutableEquatable<T> =>
object.ReferenceEquals(inst, obj) // same reference -> equal
|| !(obj is null) // this is not null but obj is -> not equal
&& obj.GetType() == inst.GetType() // obj is more derived than this -> not equal
&& inst.GetHashCode() == obj.GetHashCode() // optimization, hash codes are different -> not equal
&& obj is T o // obj cannot be cast to this type -> not equal
&& thisEquals(o);
public static int GetHashCode<T>(this T inst, ref int? hashCache, Func<int> thisHashCode) where T : IImmutableEquatable<T> {
if (hashCache is null) hashCache = thisHashCode();
return hashCache.Value;
}
}
}
I can now do:
class Base : IImmutableEquatable<Base> {
public readonly SomeImmutableType1 X;
readonly SomeImmutableType2 Y;
public Base(SomeImmutableType1 X, SomeImmutableType2 Y) => (this.X, this.Y) = (X, Y);
public bool ThisEquals(Base o) => (X, Y) == (o.X, o.Y);
public int ThisHashCode() => (X, Y).GetHashCode();
// boilerplate
public override bool Equals(object obj) => this.ImmutableEquals(obj, ThisEquals);
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);
protected int? hashCache;
public override int GetHashCode() => this.GetHashCode(ref hashCache, ThisHashCode);
}
class Derived : Base, IImmutableEquatable<Derived> {
public readonly SomeImmutableType3 Z;
readonly SomeImmutableType4 K;
public Derived(SomeImmutableType1 X, SomeImmutableType2 Y, SomeImmutableType3 Z, SomeImmutableType4 K) : base(X, Y) => (this.Z, this.K) = (Z, K);
public bool ThisEquals(Derived o) => base.ThisEquals(o) && (Z, K) == (o.Z, o.K);
public new int ThisHashCode() => (base.ThisHashCode(), Z, K).GetHashCode();
// boilerplate
public override bool Equals(object obj) => this.ImmutableEquals(obj, ThisEquals);
public bool Equals(Derived o) => object.Equals(this, o);
public override int GetHashCode() => this.GetHashCode(ref hashCache, ThisHashCode);
}
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` and `ThisHashCode`
(testing)
Problem
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 call `Derived.Equals` operators `==` and `!=` will call `Equals` rather than `ReferenceEquals` (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 ?