25

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 ?

kofifus
  • 17,260
  • 17
  • 99
  • 173
  • 1
    I'd move the comparison logic out of `Equals(Object)` and into `Equals(Base)` to avoid unnecessary casts. – Dai Feb 26 '19 at 01:56
  • but then I will loose the polymorphism which was the whole point :( – kofifus Feb 26 '19 at 02:08
  • You don't lose polymorphism. Don't delete any methods, just move the implementation. – Dai Feb 26 '19 at 02:48
  • if `Equals(Base)` will not call `Equals(Object)` then `((Base)derived1).Equals((Base)derived2)` will not call `Derived.Equals` – kofifus Feb 26 '19 at 03:04
  • 1
    What should happen if `Base a = new Base(); Base b = new Derived(); a.Equals( b )`? – Dai Feb 26 '19 at 03:16
  • it will call Base.Equals of a with (Base)b – kofifus Feb 26 '19 at 03:18
  • 4
    I think there's a fundamental problem here in that `Base.Equals(Object)` can accept an instance of `Derived` and the two can be equal if `X` and `Y` compare equal, completely ignoring the fact that `Derived` has more to it. That is to say that `new Base(1, 2).Equals(new Derived(1, 2, 3, 4))` returns `true` and I have a hard time reconciling that with calling this "polymorphic". If an object can only be equal to another instance of the same type but comparable from a common base, that feels more right and simplifies matters tremendously. – madreflection Feb 26 '19 at 04:42
  • I see your point, ideally I would like to in this case simply return false just because they are different types, but I'm not sure how to code that .. `this.GetType()==obj.GetType()` ? – kofifus Feb 26 '19 at 04:57
  • yeah that works, I edited my question and added a `GetType` check so that this case returns false ... – kofifus Feb 26 '19 at 05:06
  • 7
    Be careful - your `X`, `Y`, `Z`, and `K` variables are mutable and that's bad for overriding `GetHashCode`. A hash code should never change. – Enigmativity Feb 26 '19 at 05:52
  • Yes was just a sample amended with 'readonly' – kofifus Feb 26 '19 at 07:26
  • 1
    It seems that you want records: https://github.com/dotnet/csharplang/blob/98043cdc889303d956d540d7ab3bc4f5044a9d3b/proposals/records.md. Actually as you see from the code in the proposal, there is perhaps no way to make it much simpler than your code. – Vlad Feb 26 '19 at 11:15
  • 3
    @kofifus - `readonly` is not the same as "immutable". If you use the hash code from your `readonly` variables and they in turn compute a hash code from their mutable properties then you're still in the same position. You need to ensure that the hash code can't change throughout the object model. – Enigmativity Feb 27 '19 at 05:10
  • yeah I know this is C# mess not mine, since I have no way to declare/verify immutability I just have to use a convention and say it is.. if X is immutable my convention says all members are of immutable types – kofifus Feb 27 '19 at 05:12
  • Your `int GetHashCode(this TSrc inst, Func f)` extension method seems like a leaky abstraction. – Enigmativity Feb 27 '19 at 05:13
  • I got the idea for the hashcode optimization (using getHashCode to optimize equals for immutable classes) from Eric Lippert's answer here https://stackoverflow.com/a/54584954/460084 – kofifus Feb 27 '19 at 05:14
  • What if `derived1` and `derived2` are of different types? – Lasse V. Karlsen Feb 28 '19 at 08:52
  • 8
    I don't have time to review the code in detail, but I'll note two things, (1) you are right that this problem is harder in C# than we would like, and (2) make sure your test cases test for *all* the necessary properties of equality: that `a==b` agrees with `b==a`, that `a==a` is always true, and that transitivity is maintained; if `a==b` and `b==c` are true, then `a==c` must be true. Many implementations of equality fail to meet these criteria and then bad things happen. – Eric Lippert Feb 28 '19 at 21:10
  • 5
    An educational recent example is https://stackoverflow.com/questions/54025578/need-help-understanding-unexpected-behavior-using-linq-join-with-hashsett/54028123#54028123 -- note the OP's comment where they say that the bug is in their equality implementation, which they *insisted* to me was correct, even though in truth it did not meet the transitivity requirement. Bad things happen when you implement equality incorrectly. – Eric Lippert Feb 28 '19 at 21:13
  • Thanks Eric - this is why I am trying to wrap this all in an extension method - any chance you can look at my answer below ? – kofifus Feb 28 '19 at 21:22

4 Answers4

10

Well I guess there are two parts to you problem:

  1. executing equals at nested level
  2. restricting to the same type

Would this work? https://dotnetfiddle.net/eVLiMZ (I had to use some older syntax as it didn't compile in dotnetfiddle otherwise)

using System;


public class Program
{
    public class Base
    {
        public string Name { get; set; }
        public string VarName { get; set; }

        public override bool Equals(object o)
        {
            return object.ReferenceEquals(this, o) 
                || o.GetType()==this.GetType() && ThisEquals(o);
        }

        protected virtual bool ThisEquals(object o)
        {
            Base b = o as Base;
            return b != null
                && (Name == b.Name);
        }

        public override string ToString()
        {
            return string.Format("[{0}@{1} Name:{2}]", GetType(), VarName, Name);
        }

        public override int GetHashCode()
        {
            return Name.GetHashCode();
        }
    }

    public class Derived : Base
    {
        public int Age { get; set; }

        protected override bool ThisEquals(object o)
        {
            var d = o as Derived;
            return base.ThisEquals(o)
                && d != null
                && (d.Age == Age);
        }

        public override string ToString()
        {
            return string.Format("[{0}@{1} Name:{2} Age:{3}]", GetType(), VarName, Name, Age);
        }

        public override int GetHashCode()
        {
            return base.GetHashCode() ^ Age.GetHashCode();
        }
    }

    public static void Main()
    {
        var b1 = new Base { Name = "anna", VarName = "b1" };
        var b2 = new Base { Name = "leo", VarName = "b2" };
        var b3 = new Base { Name = "anna", VarName = "b3" };
        var d1 = new Derived { Name = "anna", Age = 21, VarName = "d1" };
        var d2 = new Derived { Name = "anna", Age = 12, VarName = "d2" };
        var d3 = new Derived { Name = "anna", Age = 21, VarName = "d3" };

        var all = new object [] { b1, b2, b3, d1, d2, d3 };

        foreach(var a in all) 
        {
            foreach(var b in all)
            {
                Console.WriteLine("{0}.Equals({1}) => {2}", a, b, a.Equals(b));
            }
        }
    }
}

aiodintsov
  • 2,545
  • 15
  • 17
  • thx @aiodintsov, I see that you added a `ThisEquals` method .. in what way is that different or better than above solution ? – kofifus Feb 28 '19 at 05:26
  • 3
    @kofifus less code, easier to read, separates concerrns, no spaghetti, base method instead of generics, no need for a supporting class, self-contained to the hierarchy, straightforward would be reasons for me to choose solution I proposed. However people are different. There is often not one single correct answer, it often depends on goals. – aiodintsov Feb 28 '19 at 07:30
  • 1
    @kofifus also it is basically based on the template method design pattern. – aiodintsov Feb 28 '19 at 07:36
  • 1
    I am personally not sure if types must match, usually it tends to be that since D is a B, from B perspective extra fields in don’t make a difference. Actually, now that I said this I may have to revisit my answer tomorrow. I fail to see how this would even become a problem unless you defined an overload for equals that causes infinite loop. Equals polymorphic to begin with. – aiodintsov Feb 28 '19 at 07:46
  • Ah I see.. it saves you defining Equals in Derived – kofifus Feb 28 '19 at 09:25
  • 'from B perspective' yes but from ie an ImmutableList no – kofifus Feb 28 '19 at 09:55
8

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 ==, !=, and Equals(). Same with the hashcode.

public class Base : IEquatable<Base>, IImmutable
{
    public readonly ImmutableType1 X;
    readonly ImmutableType2 Y;

    public Base(ImmutableType1 X, ImmutableType2 Y) => (this.X, this.Y) = (X, Y);

    // boilerplate
    public override bool Equals(object obj) => this.ImmutableEquals(obj);
    public bool Equals(Base o) => this.ImmutableEquals(o);
    public static bool operator ==(Base o1, Base o2) => o1.ImmutableEquals(o2);
    public static bool operator !=(Base o1, Base o2) => !o1.ImmutableEquals(o2);
    private int? _hashCache;
    public override int GetHashCode() => this.ImmutableHash(ref _hashCache);
}

public class Derived : Base, IEquatable<Derived>, IImmutable
{
    public readonly ImmutableType3 Z;
    readonly ImmutableType4 K;

    public Derived(ImmutableType1 X, ImmutableType2 Y, ImmutableType3 Z, ImmutableType4 K) : base(X, Y) => (this.Z, this.K) = (Z, K);

    public bool Equals(Derived other) => this.ImmutableEquals(other);
}

And the IImmutableExtensions class:

public static class IImmutableExtensions
{
    public static bool ImmutableEquals(this IImmutable o1, object o2)
    {
        if (ReferenceEquals(o1, o2)) return true;
        if (o2 is null || o1.GetType() != o2.GetType() || o1.GetHashCode() != o2.GetHashCode()) return false;

        foreach (var tProp in GetImmutableFields(o1))
        {
            var test = tProp.GetValue(o1)?.Equals(tProp.GetValue(o2));
            if (test is null) continue;
            if (!test.Value) return false;
        }
        return true;
    }

    public static int ImmutableHash(this IImmutable o, ref int? hashCache)
    {
        if (hashCache is null)
        {
            hashCache = 0;

            foreach (var tProp in GetImmutableFields(o))
            {
                hashCache = HashCode.Combine(hashCache.Value, tProp.GetValue(o).GetHashCode());
            }
        }
        return hashCache.Value;
    }

    private static IEnumerable<FieldInfo> GetImmutableFields(object o)
    {
        var t = o.GetType();
        do
        {
            var fields = t.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).Where(field => field.IsInitOnly);

            foreach(var field in fields)
            {
                yield return field;
            }
        }
        while ((t = t.BaseType) != typeof(object));
    }
}

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 methods Equals(object) and Equals(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 overridden ValuesEqual. 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() and GetHashCode() 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.

public class Base : IEquatable<Base>, IImmutable
{
    public readonly ImmutableType1 X;
    readonly ImmutableType2 Y;

    public Base(ImmutableType1 X, ImmutableType2 Y) => 
      (this.X, this.Y) = (X, Y);

    protected virtual IStructuralEquatable GetImmutableTuple() => (X, Y);

    // boilerplate
    public override bool Equals(object o) => IsEqual(o as Base);
    public bool Equals(Base o) => IsEqual(o);
    public static bool operator ==(Base o1, Base o2) => o1.IsEqual(o2);
    public static bool operator !=(Base o1, Base o2) => !o1.IsEqual(o2);
    public override int GetHashCode() => hashCache is null ? (hashCache = GetImmutableTuple().GetHashCode()).Value : hashCache.Value;
    protected bool IsEqual(Base obj) => ReferenceEquals(this, obj) || !(obj is null) && GetType() == obj.GetType() && GetHashCode() == obj.GetHashCode() && GetImmutableTuple() != obj.GetImmutableTuple();
    protected int? hashCache;
}

public class Derived : Base, IEquatable<Derived>, IImmutable
{
    public readonly ImmutableType3 Z;
    readonly ImmutableType4 K;

    public Derived(ImmutableType1 X, ImmutableType2 Y, ImmutableType3 Z, ImmutableType4 K) : base(X, Y) => 
      (this.Z, this.K) = (Z, K);

    protected override IStructuralEquatable GetImmutableTuple() => (base.GetImmutableTuple(), K, Z);

    // boilerplate
    public bool Equals(Derived o) => IsEqual(o);
}
Jerry
  • 1,477
  • 7
  • 14
  • I like this, if you add ValueEqual into IImutable then ImmutableEquals an be in an extension method ? – kofifus Mar 02 '19 at 08:00
  • Overall though I'm not sure this is better or simpler ? You replaced having to remember to cast to Object with having to remember to call ValueEquals instead of Equals and added both ValueEquals and ImmutableEquals .. not sure – kofifus Mar 02 '19 at 08:12
  • @kofifus I have renamed ```ImmutableEquals``` to ```IsEqual```, and ```GetValueTuple``` to ```GetHashTuple``` for clarity. ```IsEqual``` only needs to be in the base class and calls the overridden ```CompareValues```. You are right that you have to remember to override this method or only the base objects will be compared. – Jerry Mar 02 '19 at 08:33
  • did some edits to your answer .. also why have IsEqual separate ? you can put it's code in Equals(object obj) and then have Equals(Base o) and Equals(Derived o) call object.Equals(this, o); – kofifus Mar 02 '19 at 12:15
  • also Derived.CompareValues can do base.CompareValues(o) :) – kofifus Mar 02 '19 at 12:16
  • IsEqual is separate to solve the "is confusing and error prone" problem when you have two methods with signatures that can both match Equals(Base o). Do you want to call base.Equals(o) or base.Equals(o as object)? – Jerry Mar 02 '19 at 17:15
  • note another issue, if ReferenceEquals return true you still do equality even though it's not needed – kofifus Mar 03 '19 at 00:48
  • 1
    No, with `||` if the left side evaluates to true the right side is never evaluated. https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/conditional-or-operator. – Jerry Mar 03 '19 at 02:10
  • I have done further edits to my answer above if you want to have a look .. combining both approaches I can remove the need to cast in ValueEquals – kofifus Mar 03 '19 at 02:18
  • 1
    I don't see any way to further simplify this, it fits all of your criteria. It is polymorphic, the code is relatively simple to read, none of the boiler code is duplicated in the derived class, implements `IEquatable` and overrides `Equals(object)`, `==`, and `!=` and compares equality based on the values of your immutable objects. The only think you need to declare in a derived class are the variables which are used for the comparison. – Jerry Mar 03 '19 at 02:25
  • 1
    At some point there has to be exactly one cast from `object` to `Base`, and with this solution it is in the boiler code and never has to be repeated. – Jerry Mar 03 '19 at 02:27
  • You could build it in your constructor if you like. It is built each time with immutable values making it inherently immutable. That is a design decision and if you expect these object to be compared to each other the majority of the time it would make sense to do that, and probably just calculate the hashcode in the constructor at the same time. However, you would need to do this in each derived class as the constructor for your Base class is called before `X` and `Y` are assigned. The other option would be to lazy load the tuple on first call. It wouldn't be readonly,but it would be private – Jerry Mar 03 '19 at 02:33
  • Think of it as a Tuple of immutable objects, not an immutable tuple. Perhaps a name change would clarify it? – Jerry Mar 03 '19 at 02:39
  • You mean caching the tuple? .. this can end up too big .. I think your solution is great as is .. if nothing better comes along I'll accept it .. thx ! – kofifus Mar 03 '19 at 03:13
  • I am worried about `operator ==(Base o1, Base o2) => o1.IsEqual(o2);` wouldn't `operator ==(Base o1, Base o2) => Object.Equals(o1, o2);` be better ? can't there be a case where `o1` is null ? – kofifus Mar 03 '19 at 04:05
  • Also note that you lost the ability of having private members, derived always have access through GetImmutableTuple – kofifus Mar 03 '19 at 06:04
  • @kofifus See the revised answer, this came to me based on my reflection answer below and your updated answer. – Jerry Mar 04 '19 at 02:40
  • thx .. to be honest I don't like this use of reflection .. beside the performance hit, you are making a lot of assumptions about the class- it is common to have getters that you don't want to participate in Equality (ie `string FullName => FirstName+","+LastName;`) which will break your code – kofifus Mar 04 '19 at 03:48
  • 1
    It only looks at readonly fields (immutable). Though I agree, everything that I attempted was a trade off between simplicity and performance. That fastest being very similar to the solution you currently have. With reflection comparing objects that have the same immutable values performed at ~1000/ms where as the tuple and explicit solutions were ~13,000/ms. (6700k i7). They all performed similarly when the immutable values were different as it would short circuit at the hashcode. – Jerry Mar 04 '19 at 04:32
  • I ended up choosing my answer but gave you the bounty :) I feel using reflection can be a good solution in some cases thanks ! also, can you please remove your comments on my answer above as they refer to previous revisions ... – kofifus Mar 07 '19 at 02:02
6

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)

kofifus
  • 17,260
  • 17
  • 99
  • 173
3

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 and AutoCompare() 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.

public class Base : ComparableImmutable, IEquatable<Base>, IImmutable
{
    [Immutable]
    public ImmutableType1 X { get; set; }

    [Immutable]
    readonly ImmutableType2 Y;

    public Base(ImmutableType1 X, ImmutableType2 Y) => (this.X, this.Y) = (X, Y);

    public bool Equals(Base o) => AutoCompare(o);
}

public class Derived : Base, IEquatable<Derived>, IImmutable
{
    [Immutable]
    public readonly ImmutableType3 Z;

    [Immutable]
    readonly ImmutableType4 K;

    public Derived(ImmutableType1 X, ImmutableType2 Y, ImmutableType3 Z, ImmutableType4 K)
        : base(X, Y)
        => (this.Z, this.K) = (Z, K);

    public bool Equals(Derived o) => AutoCompare(o);
}

[AttributeUsage(validOn: AttributeTargets.Field | AttributeTargets.Property)]
public class ImmutableAttribute : Attribute { }

public abstract class ComparableImmutable
{
    static BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly;

    protected int? hashCache;

    public override int GetHashCode()
    {
        if (hashCache is null)
        {
            hashCache = 0;
            var type = GetType();

            do
            {
                foreach (var field in type.GetFields(flags).Where(field => Attribute.IsDefined(field, typeof(ImmutableAttribute))))
                    hashCache = HashCode.Combine(hashCache, field.GetValue(this));

                foreach (var property in type.GetProperties(flags).Where(property => Attribute.IsDefined(property, typeof(ImmutableAttribute))))
                    hashCache = HashCode.Combine(hashCache, property.GetValue(this));

                type = type.BaseType;
            }
            while (type != null);
        }

        return hashCache.Value;
    }

    protected bool AutoCompare(object obj2)
    {
        if (ReferenceEquals(this, obj2)) return true;

        if (obj2 is null
            || GetType() != obj2.GetType()
            || GetHashCode() != obj2.GetHashCode())
            return false;

        var type = GetType();

        do
        {
            foreach (var field in type.GetFields(flags).Where(field => Attribute.IsDefined(field, typeof(ImmutableAttribute))))
            {
                if (field.GetValue(this) != field.GetValue(obj2))
                {
                    return false;
                }
            }

            foreach (var property in type.GetProperties(flags).Where(property => Attribute.IsDefined(property, typeof(ImmutableAttribute))))
            {
                if (property.GetValue(this) != property.GetValue(obj2))
                {
                    return false;
                }
            }

            type = type.BaseType;
        }
        while (type != null);

        return true;
    }

    public override bool Equals(object o) => AutoCompare(o);
    public static bool operator ==(Comparable o1, Comparable o2) => o1.AutoCompare(o2);
    public static bool operator !=(Comparable o1, Comparable o2) => !o1.AutoCompare(o2);
}
Jerry
  • 1,477
  • 7
  • 14