With two immutable classes Base and Derived (which derives from Base) I want to define Equality so that
equality is always polymorphic - that is ((Bas
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(this T inst, object obj, Func thisEquals) where T : IEquatable =>
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 {
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 {
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 : IEquatable { };
public static partial class ExtensionMethods {
public static bool ImmutableEquals(this T inst, object obj, Func thisEquals) where T : IImmutableEquatable =>
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(this T inst, ref int? hashCache, Func thisHashCode) where T : IImmutableEquatable {
if (hashCache is null) hashCache = thisHashCode();
return hashCache.Value;
}
}
}
I can now do:
class Base : IImmutableEquatable {
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 {
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)