平等和多态

kof*_*fus 24 c# c#-7.0 c#-7.3

有两个不可变类Base和Derived(派生自Base)我想定义Equality这样

  • 平等总是多态的 - ((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);    }
Run Code Online (Sandbox Code Playgroud)

这里的一切最终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);
}
Run Code Online (Sandbox Code Playgroud)

这基本上是相同的,除了一个问题 - 当base.Equals我打电话给我base.Equals(object)而不是base.Equals(Derived)(这将导致无休止的递归).

Equals(C)在这个实现中也将做一些装箱/拆箱但这对我来说是值得的.

我的问题是 -

首先是正确的吗?我的(测试)似乎暗示它是,但是C#在平等方面是如此困难,我只是不确定..是否有任何情况这是错误的?

第二 - 这好吗?是否有更好的清洁方法来实现这一目标?

aio*_*sov 10

好吧,我想问题有两个部分:

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

Run Code Online (Sandbox Code Playgroud)

  • @kofifus的代码更少,更易于阅读,可以分隔concerrn,没有意大利面条,可以用基本方法代替泛型,不需要支持类,独立于层次结构,简单易懂,这就是我选择我提出的解决方案的原因。但是人是不同的。通常没有一个正确的答案,它通常取决于目标。 (3认同)

Jer*_*rry 8

这种使用反射的比较方法比扩展方法更简单。它还使私人成员保持私密。

所有的逻辑都在IImmutableExtensions类中。它只是查看哪些字段是只读的,并将它们用于比较。

您不需要基类或派生类中的方法来进行对象比较。只需调用扩展方法ImmutableEquals时,你要重写==!=Equals()。与哈希码相同。

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);
}
Run Code Online (Sandbox Code Playgroud)

IImmutableExtensions班级:

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));
    }
}
Run Code Online (Sandbox Code Playgroud)

旧答案:(我将留作参考)

根据您所说的关于必须强制转换的说法,在object我看来,方法Equals(object)Equals(Base)从派生类中调用时过于模棱两可。

这对我说,应该将逻辑从这两个类中移出,使用一种可以更好地描述我们意图的方法。

相等将保持多态性,就像ImmutableEquals在基类中将其称为Overrided一样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);
}
Run Code Online (Sandbox Code Playgroud)


kof*_*fus 6

可以结合使用扩展方法和一些锅炉代码来简化代码。这消除了几乎所有的痛苦,并使类专注于比较其实例,而不必处理所有特殊情况:

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);
  }
}
Run Code Online (Sandbox Code Playgroud)

我现在可以做:

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);
}
Run Code Online (Sandbox Code Playgroud)

很好,没有强制转换或空值检查,并且所有实际工作都在中明确分隔ThisEquals
测试


对于不可变的类,可以通过缓存哈希码并在哈希码不同的情况下将其用于等于快捷方式相等来进一步优化:

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;
    }
  }
}
Run Code Online (Sandbox Code Playgroud)


我现在可以做:

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);
}
Run Code Online (Sandbox Code Playgroud)

这是不是太糟糕-有更多的复杂性,但它是我只是复制粘贴都只是样板..逻辑是明确分开ThisEquals,并ThisHashCode

(测试中)