Равенство и полиморфизм

С двумя неизменяемыми классами Base и Derived (который является производным от Base) я хочу определить равенство так, чтобы

  • равенство всегда полиморфно - то есть ((Base)derived1).Equals((Base)derived2) позвоню Derived.Equals

  • операторы == а также != позвоню Equals скорее, чем ReferenceEquals (равенство значений)

Что я сделал:

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);    }

Здесь все заканчивается Equals(object) который всегда полиморфен, поэтому обе цели достигнуты.

Затем я получаю так:

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);
}

Что в основном то же самое, за исключением одного гоча - при звонке base.Equals Я звоню base.Equals(object) и не base.Equals(Derived) (что приведет к бесконечной рекурсии).

Также Equals(C) будет в этой реализации делать некоторые бокс / распаковку, но это того стоит для меня.

Мои вопросы -

Во-первых, это правильно? мои ( тестирование), кажется, предполагают, что это так, но из-за того, что C# настолько сложен в равенстве, я просто не уверен больше... есть ли случаи, когда это не так?

и второе - это хорошо? Есть ли более чистые способы достичь этого?

4 ответа

Ну, я думаю, есть две проблемы для вас:

  1. выполнение равных на вложенном уровне
  2. ограничиваясь тем же типом

Будет ли это работать? https://dotnetfiddle.net/eVLiMZ(мне пришлось использовать какой-то более старый синтаксис, так как в противном случае он не компилировался в dotnetfiddle)

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));
            }
        }
    }
}

Исходя из того, что вы говорили о необходимости object мне пришло в голову, что методы Equals(object) а также Equals(Base) были слишком двусмысленными, когда вызывали их из производного класса.

Это сказало мне, что логику следует перенести из обоих классов в метод, который лучше описывал бы наши намерения.

Равенство останется полиморфным как ImmutableEquals в базовом классе назовем переопределенный ValuesEqual, Здесь вы можете решить в каждом производном классе, как сравнивать равенство.

Это ваш код с этой целью.

Пересмотренный ответ:

Мне пришло в голову, что вся наша логика в IsEqual() а также GetHashCode() будет работать, если мы просто предоставим кортеж, содержащий неизменяемые поля, которые мы хотим сравнить. Это позволяет избежать дублирования кода в каждом классе.

Разработчик должен создать производный класс для переопределения GetImmutableTuple(), Не используя отражения (см. Другой ответ), я чувствую, что это меньшее из всех зол.

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);
}

Пока что лучшее, что я могу предложить - это метод расширения. Это устраняет почти всю боль и заставляет классы сосредоточиться на сравнении своих участников без необходимости заниматься всеми особыми крайними случаями.

Для неизменяемых классов возможна дальнейшая оптимизация путем кэширования хеш-кода и использования его в "Равно" для сокращения равенства, если хеш-коды отличаются:

namespace System.Immutable {

  public interface IImmutable<TSrc> {
    bool ValueEquals(TSrc o);
    int ValueHashCode();
  };

  public static partial class ExtensionMethods {
    public static bool ImmutableEquals<TSrc>(this TSrc inst, object obj) where TSrc : IEquatable<TSrc>, IImmutable<TSrc> {
      if (object.ReferenceEquals(inst, obj)) return true; // same reference ->  equal
      if (obj is null) return false; // this is not null but obj is -> not equal
      if (obj.GetType() != inst.GetType()) return false; // obj is more derived than this -> not equal
      if (inst.GetHashCode() != obj.GetHashCode()) return false; // optimization, hash codes are different -> not equal
      if (!(obj is TSrc o)) return false; // obj cannot be cast to this type -> not equal
      return inst.ValueEquals(o);
    }

    public static int ImmutableHash<TSrc>(this TSrc inst, ref int? hashCache) where TSrc : IEquatable<TSrc>, IImmutable<TSrc> {
      if (hashCache is null) hashCache = inst.ValueHashCode();
      return hashCache.Value;
    }
  }
}


Теперь я могу сделать:

class Base: IEquatable<Base>, IImmutable<Base> {
  public readonly int X;
  readonly int Y;
  public Base(int X, int Y) => (this.X, this.Y) = (X, Y); 

  public bool ValueEquals(Base o) => (X, Y) == (o.X, o.Y);
  public int ValueHashCode() => (X, Y).GetHashCode();

  // 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) => object.Equals(o1, o2);
  public static bool operator !=(Base o1, Base o2) => !object.Equals(o1, o2);
  protected int? hashCache;
  public override int GetHashCode() => this.ImmutableHash(ref hashCache);
}

class Derived : Base, IEquatable<Derived>, IImmutable<Derived> {
  public readonly int Z;
  readonly int K;
  public Derived(int X, int Y, int Z, int K) : base(X, Y) => (this.Z, this.K) = (Z, K); 

  public bool ValueEquals(Derived o) => base.ValueEquals(o) && (Z, K) == (o.Z, o.K);
  public new int ValueHashCode() => (base.ValueHashCode(), Z, K).GetHashCode();

  // boilerplate
  public override bool Equals(object obj) => this.ImmutableEquals(obj);
  public bool Equals(Derived o) => this.ImmutableEquals(o); 
  public override int GetHashCode() => this.ImmutableHash(ref hashCache);
}

Что не так уж и плохо, все уродство в методе расширения и коде котла, который просто вырезан и вставлен, а логика четко отделена

(тестирование)

Другим методом будет использование Reflection для автоматического сравнения всех ваших полей и свойств. Вы просто должны украсить их Immutable атрибут и AutoCompare() позаботится об остальном.

Это также будет использовать Reflection для построения HashCode на основе ваших полей и свойств, украшенных Immutable, а затем кэшировать его для оптимизации сравнения объектов.

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);
}
Другие вопросы по тегам