相等性和多态性
Equality and polymorphism
有两个不可变的 类 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); }
这里一切都以 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)
将在此实现中做一些 boxing/unboxing 但这对我来说是值得的。
我的问题是 -
首先这是正确的吗?我的 (testing) 似乎暗示它是这样,但是由于 C# 在平等方面如此困难,我只是不确定了..在任何情况下这是错误的吗?
其次 - 这样好吗?有没有更好更清洁的方法来实现这一目标?
嗯,我想你的问题有两个部分:
- 在嵌套级别执行 equals
- 限制为同一类型
这行得通吗? 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));
}
}
}
}
可以结合使用扩展方法和一些样板代码来简化代码。这消除了几乎所有的痛苦,让 类 专注于比较它们的实例,而不必处理所有特殊的边缘情况:
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);
}
}
我现在可以做到:
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);
}
这很好,没有转换或 null 检查,所有实际工作在 ThisEquals
中清楚地分开了。
(testing)
对于不可变 类,可以通过缓存哈希码并在 Equals 中使用它来进一步优化,如果哈希码不同,则可以实现快捷方式相等:
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;
}
}
}
我现在可以做到:
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);
}
这还不错 - 有更多的复杂性,但它只是样板,我只是剪切和粘贴..逻辑在 ThisEquals
和 ThisHashCode
中明显分开
这种使用Reflection的比较方法,比扩展方法更简单。它还使私有成员保持私有。
所有逻辑都在 IImmutableExtensions
class 中。它只是查看哪些字段是只读的并将它们用于比较。
您不需要基类或派生类 class 中的方法来进行对象比较。覆盖 ==
、!=
和 Equals()
时,只需调用扩展方法 ImmutableEquals
。与哈希码相同。
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);
}
和 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));
}
}
旧答案:(留作参考)
根据你所说的必须强制转换为 object
,我想到从派生的 class.
这对我说,逻辑应该从两个 classes 中移出,转移到一种能更好地描述我们意图的方法。
等式将保持多态性,因为基 class 中的 ImmutableEquals
将调用重写的 ValuesEqual
。您可以在此处决定每个派生 class 如何比较相等性。
这是您按照该目标重构的代码。
修改后的答案:
我突然想到,如果我们简单地提供一个包含我们想要比较的不可变字段的元组,我们在 IsEqual()
和 GetHashCode()
中的所有逻辑都会起作用。这避免了在每个 class.
中重复这么多代码
由创建派生 class 的开发人员来覆盖 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);
}
另一种方法是使用反射来自动比较所有字段和属性。您只需用 Immutable
属性装饰它们,AutoCompare()
会处理其余的事情。
这也会根据你Immutable
装饰的字段和属性使用Reflection构建一个HashCode,然后缓存它来优化对象比较。
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);
}
有两个不可变的 类 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); }
这里一切都以 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)
将在此实现中做一些 boxing/unboxing 但这对我来说是值得的。
我的问题是 -
首先这是正确的吗?我的 (testing) 似乎暗示它是这样,但是由于 C# 在平等方面如此困难,我只是不确定了..在任何情况下这是错误的吗?
其次 - 这样好吗?有没有更好更清洁的方法来实现这一目标?
嗯,我想你的问题有两个部分:
- 在嵌套级别执行 equals
- 限制为同一类型
这行得通吗? 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));
}
}
}
}
可以结合使用扩展方法和一些样板代码来简化代码。这消除了几乎所有的痛苦,让 类 专注于比较它们的实例,而不必处理所有特殊的边缘情况:
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);
}
}
我现在可以做到:
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);
}
这很好,没有转换或 null 检查,所有实际工作在 ThisEquals
中清楚地分开了。
(testing)
对于不可变 类,可以通过缓存哈希码并在 Equals 中使用它来进一步优化,如果哈希码不同,则可以实现快捷方式相等:
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;
}
}
}
我现在可以做到:
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);
}
这还不错 - 有更多的复杂性,但它只是样板,我只是剪切和粘贴..逻辑在 ThisEquals
和 ThisHashCode
这种使用Reflection的比较方法,比扩展方法更简单。它还使私有成员保持私有。
所有逻辑都在 IImmutableExtensions
class 中。它只是查看哪些字段是只读的并将它们用于比较。
您不需要基类或派生类 class 中的方法来进行对象比较。覆盖 ==
、!=
和 Equals()
时,只需调用扩展方法 ImmutableEquals
。与哈希码相同。
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);
}
和 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));
}
}
旧答案:(留作参考)
根据你所说的必须强制转换为 object
,我想到从派生的 class.
这对我说,逻辑应该从两个 classes 中移出,转移到一种能更好地描述我们意图的方法。
等式将保持多态性,因为基 class 中的 ImmutableEquals
将调用重写的 ValuesEqual
。您可以在此处决定每个派生 class 如何比较相等性。
这是您按照该目标重构的代码。
修改后的答案:
我突然想到,如果我们简单地提供一个包含我们想要比较的不可变字段的元组,我们在 IsEqual()
和 GetHashCode()
中的所有逻辑都会起作用。这避免了在每个 class.
由创建派生 class 的开发人员来覆盖 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);
}
另一种方法是使用反射来自动比较所有字段和属性。您只需用 Immutable
属性装饰它们,AutoCompare()
会处理其余的事情。
这也会根据你Immutable
装饰的字段和属性使用Reflection构建一个HashCode,然后缓存它来优化对象比较。
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);
}