Этот метод сравнения с использованием Reflection, который, кроме методов расширения, является более простым.Кроме того, приватные члены остаются закрытыми.
Вся логика в классе 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);
}
И класс 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));
}
}
Старый ответ: (я оставлю это для справки)
На основании чеговы говорили о необходимости приводить к 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);
}