我有两个这样的类:
class Base : IEquatable<Base>
{
int A;
public Base(int a)
{
A = a;
}
public bool Equals([AllowNull] Base other)
{
if (other is null)
return false;
else
return (A == other.A);
}
public override bool Equals(object other)
{
return Equals(other as Base);
}
public static bool operator==(Base one, Base two)
{
return (one is null) ? (two is null) : one.Equals(two);
}
public static bool operator !=(Base one, Base two)
{
return !(one == two);
}
public override int GetHashCode()
{
return HashCode.Combine(A);
}
}
class Derived : Base, IEquatable<Derived>
{
int B;
public Derived(int a, int b)
: base(a)
{
B = b;
}
public bool Equals([AllowNull] Derived other)
{
if (other is null)
return false;
else
return (A == other.A) && (B == other.B);
}
public override bool Equals(object other)
{
return Equals(other as Derived);
}
public static bool operator==(Derived one, Derived two)
{
return (one is null) ? (two is null) : one.Equals(two);
}
public static bool operator !=(Derived one, Derived two)
{
return !(one == two);
}
public override int GetHashCode()
{
return HashCode.Combine(A, B);
}
}
那么现在如果我有一个这样的字典:
Dictionary<Base, string> Dict = new Dictionary<Base, string>();
我要确保字典正确地分隔所有不同的值:
Dict[new Base(1)] = "one";
Dict[new Derived(1, 2)] = "one, two";
Dict[new Derived(1, 3)] = "one, three";
Assert.AreEqual(3, Dict.Count);
Derived der13again = new Derived(1, 3);
Assert.IsTrue(Dict.ContainsKey(der13again));
// Notice this Assert is why I had to override IEquatable<> so that the
// keys are compared by value, not by reference
我需要确保它在所有情况下都能工作,即使是在有哈希冲突的罕见情况下。为了强制哈希冲突,我编辑了Derived.GetHashCode(),使其只返回HashCode.Combine(a)。当我这样做并添加一些调试时,第一个Assert失败(字典只包含1个条目),并且我得到以下输出:
// Adding [Base 1] to dict
Base.GetHashCode() returning -1259686161 [this=[Base 1]]
dict has 1 members
// So far so good
// Step 2: Adding [Derived 1, 2] to dict
Derived.GetHashCode() returning -1259686161 [this=[Derived 1, 2]]
// Dictionary notices the hash collision and wonders if it's actually
// the same key value, so it calls Equals() to check if old key == new key
// BUT USES BASE.EQUALS
Base.Equals(Base other=[Derived 1, 2])
// Dictionary decides the two keys are identical, so updates the value:
dict has 1 members
dict[b]=one, two
dict[d1]=one, two
// Adding [Derived 1, 3] to dict
Derived.GetHashCode() returning -1259686161 [this=[Derived 1, 3]]
// Again, hash collision, Dictionary wonders if it's the same key
Base.Equals(Base other=[Derived 1, 3])
// Decides it's the same key so just updates the value
dict has 1 members
dict[b]=one, three
dict[d1]=one, three
dict[d2]=one, three
编辑:根据Jon Skeet的评论,我更新了Base.Equals()函数如下:
public bool Equals([AllowNull] Base other)
{
if (other is null)
return false;
else if (GetType() != other.GetType())
return false;
else
return (A == other.A);
}
这很有帮助,因为现在我的字典中有两个对象,但仍然不是3(字典仍然使用Base。=用于比较两个派生值,从而发现它们相等)
如何让字典正确处理派生类?
Dictionary<TKey, TValue>
,如果您不传递自定义的EqualityComparer,将调用EqualityComparer<TKey>.Default
,这将最终成为IEquatable<TKey>
的包装器,或者更具体地说,在您的情况下IEquatable<Base>
。
看到https://referencesource.microsoft.com/mscorlib/系统/收藏/一般/dictionary.cs, 94
它不处理任何派生类型。它只关心IEquatable<Base>
的实现。
你能做的是在你的派生类型上显式地重新实现该接口,以改变该行为。或者更简单一点,让这些基方法成为虚方法。
但这意味着,派生类型的行为不同于它们的基类型,从而违反了Liskov替换原则。如果我没弄错的话。
一个可能的解决方案,基于CSharpie的想法,是有派生实现等价如下:
class Derived : Base, IEquatable<Derived>, IEquatable<Base>
{
// Add a new function:
public new bool Equals([AllowNull] Base other)
{
if (other is Derived der)
return Equals(der);
return false;
}
// This works if Base.Equals(Base) contains "else if (GetType() != other.GetType()) return false;"
另一种方法是将Base. equals (Base)设为虚值,并在Derived中重写它。
我已经验证了这两种解决方案都有效,但它们在两个方面都不是最优的:
- 由于本练习的目的是使Dictionary正常工作,因此要求更改Base/Derived层次结构是不对的
- 最好有一个"通用"的解决方案,而不是要求改变每一个这样的类层次
- 如果类层次结构更复杂——例如,如果我有"class Fred: Derived"和类George: Fred",那么George就必须重写IEquatable
和IEquatable 有把握成功
CSharpie提到的另一个解决方案看起来像这样:
public class BaseComparer<T> : IEqualityComparer<T>
{
static readonly Dictionary<Type, IEqualityComparer> Comparers = new Dictionary<Type, IEqualityComparer>();
static readonly object ComparersLock = new object();
public bool Equals([AllowNull] T x, [AllowNull] T y)
{
if (x is null)
return (y is null);
else if (y is null)
return false;
else if (x.GetType() != y.GetType())
return false;
else if (x.GetType() == typeof(T))
return EqualityComparer<T>.Default.Equals(x, y);
IEqualityComparer? comparer = null;
lock (ComparersLock)
Comparers.TryGetValue(x.GetType(), out comparer);
if (comparer == null)
{
var tec = typeof(EqualityComparer<>);
Type specific = tec.MakeGenericType(x.GetType());
var defProp = specific.GetProperty("Default");
if (defProp == null)
throw new Exception("No Default property on " + specific);
var value = defProp.GetValue(null);
if (value is IEqualityComparer comp)
{
comparer = comp;
lock (ComparersLock)
Comparers[x.GetType()] = comparer;
}
else
throw new Exception("No value for " + defProp);
}
return comparer.Equals(x, y);
}
public int GetHashCode([DisallowNull] T obj)
{
return obj.GetHashCode();
}
}
这是很好的,因为它不需要任何额外的代码到Base或Derived,但它确实有一点反射黑客。
保持常规Equals
方法的功能:
class Base : IEquatable<Base>
{
// ...
public bool Equals([AllowNull] Base other)
{
return Equals((object) other);
}
public override bool Equals(object other)
{
var that = other as Base;
return that != null
&& this.GetType == that.GetType
&& this.A == that.A;
}
// ...
}
class Derived : Base, IEquatable<Derived>
{
// ...
public bool Equals([AllowNull] Derived other)
{
return Equals((object) other)
}
public override bool Equals(object other)
{
var that = other as Derived;
return base.Equals(other)
&& this.B == that.B;
}
// ...
}