【问题标题】:Dictionary on derived classes派生类字典
【发布时间】:2021-04-26 02:42:51
【问题描述】:

我有两个这样的课程:

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 正确处理派生类?

【问题讨论】:

  • 你到底想用那本词典实现什么?
  • 这是一个虚构的例子,展示了问题的相关部分。实际问题是 (1) 太长而无法包含在 stackoverflow 帖子中,以及 (2) 专有。
  • 最好了解一下哈希表是如何处理冲突的。这不是你想象的那样。
  • 你说的很对,我已经调整了我原来的帖子,不暗示 Equals 是正常哈希冲突过程的一部分。
  • 我相信你的Equals 实现基本上被破坏了——因为“x.Equals(y) 返回与y.Equals(x)”相同的值的requirement 被违反了。 base.Equals(der1) 将返回 true,而 der1.Equals(base) 将返回 false。一旦你有一个一致的平等实现,一切都应该没问题。

标签: c# dictionary derived-class


【解决方案1】:

Dictionary&lt;TKey, TValue&gt;,如果您没有通过自定义 EqualityComparer,将调用 EqualityComparer&lt;TKey&gt;.Default,这将最终成为 IEquatable&lt;TKey&gt; 的包装器,或者在您的情况下更具体的 IEquatable&lt;Base&gt;

https://referencesource.microsoft.com/#mscorlib/system/collections/generic/dictionary.cs,94

它不处理任何派生类型。它所关心的只是IEquatable&lt;Base&gt; 的实现。

您可以做的是在派生类型上显式地重新实现该接口,以改变该行为。或者更简单,将这些基本方法虚拟化。

但这意味着,如果我没记错的话,派生类型的行为与其基类型不同,从而导致违反Liskov subsitution principle.

【讨论】:

    【解决方案2】:

    根据 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 代码,但它确实有一点反射黑客攻击。

    【讨论】:

    • 我不明白为什么当你强制它使用 EqualityComparer 时你甚至使用 IEquatable,这也回退到执行 object.Equals 和 object.GetHashcode 的 DefaultComparer。我认为您错过了 IEquatable 和 IEqualityComparer 工作原理的全部要点。
    • 很抱歉一直在改变,但正如异端猴子指出的那样,原来的帖子已经变得冗长而复杂。我已经更新了我原来的帖子,简要解释了我为什么使用 IEquatable
    • 我刚刚注意到,您使用 new 创建了一个新的 equals 方法。您想使该方法成为虚拟方法并覆盖它。不要使用新的。
    • 我添加了一个关于使 Base.Equals(Base) 虚拟化的注释,这当然也可以。
    【解决方案3】:

    只需将功能保留在常规 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;
        }
        // ...
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-08-22
      • 2015-12-27
      • 2020-02-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-07-05
      相关资源
      最近更新 更多