如何解决派生类字典
我有两个这样的类:
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.Equals 来比较两个派生值,因此发现它们相等)
如何让 Dictionary 正确处理派生类?
解决方法
Dictionary<TKey,TValue>
,如果您不传递自定义的 EqualityComparer,将调用 EqualityComparer<TKey>.Default
,这将最终成为 IEquatable<TKey>
或更具体的包装器 IEquatable<Base>
.
见https://referencesource.microsoft.com/#mscorlib/system/collections/generic/dictionary.cs,94
它不处理任何派生类型。它所关心的只是 IEquatable<Base>
的实现。
您可以做的是在您的派生类型上明确地重新实现该接口,以改变该行为。或者更简单,让这些 Base-methods 虚拟化。
但这意味着,如果我没记错的话,派生类型的行为与其基类型不同,从而导致违反 Liskov subsitution principle.。
,基于 CSharpie 的想法,一种可能的解决方案是让 Derived 实现 IEquatable,如下所示:
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”和“class George : Fred”,那么 George 将不得不重写 IEquatable
、IEquatable 、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;
}
// ...
}
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。