派生类的字典



我有两个这样的类:

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;
}
// ...
}

相关内容

  • 没有找到相关文章

最新更新