【问题标题】:Using a class versus struct as a dictionary key使用类与结构作为字典键
【发布时间】:2013-05-04 13:00:19
【问题描述】:

假设我有以下类和结构定义,并将它们分别用作字典对象中的键:

public class MyClass { }
public struct MyStruct { }

public Dictionary<MyClass, string> ClassDictionary;
public Dictionary<MyStruct, string> StructDictionary;

ClassDictionary = new Dictionary<MyClass, string>();
StructDictionary = new Dictionary<MyStruct, string>();

为什么会这样:

MyClass classA = new MyClass();
MyClass classB = new MyClass();
this.ClassDictionary.Add(classA, "Test");
this.ClassDictionary.Add(classB, "Test");

但这会在运行时崩溃:

MyStruct structA = new MyStruct();
MyStruct structB = new MyStruct();
this.StructDictionary.Add(structA, "Test");
this.StructDictionary.Add(structB, "Test");

它说密钥已经存在,正如预期的那样,但仅适用于结构。该类将其视为两个单独的条目。我认为这与作为参考与价值的数据有关,但我想更详细地解释原因。

【问题讨论】:

  • 你忽略了classAclassBstructAstructB 是什么。你能添加那个代码吗?
  • 您没有显示实例的来源,但无论哪种方式,对于您打算用作键的类,您都希望覆盖 GetHashCode 和 Equals 方法。
  • 已添加实例定义。谢谢!

标签: c# class dictionary struct equality


【解决方案1】:

Dictionary&lt;TKey, TValue&gt; 使用IEqualityComparer&lt;TKey&gt; 来比较密钥。如果在构造字典时没有明确指定比较器,它将使用EqualityComparer&lt;TKey&gt;.Default

由于MyClassMyStruct 都没有实现IEquatable&lt;T&gt;,默认的相等比较器将调用Object.EqualsObject.GetHashCode 来比较实例。 MyClass 派生自 Object,因此实现将使用引用相等进行比较。另一方面,MyStruct 派生自System.ValueType(所有结构的基类),因此它将使用ValueType.Equals 来比较实例。此方法的文档说明如下:

ValueType.Equals(Object) 方法覆盖 Object.Equals(Object) 并为 .NET Framework 中的所有值类型提供值相等的默认实现。

如果当前实例和obj的字段都不是引用类型,则Equals方法对内存中的两个对象进行逐字节比较。否则,它使用反射来比较obj和这个实例的对应字段。

如果“[字典]中已存在具有相同键的元素”,则IDictionary&lt;TKey, TValue&gt;.Add 会引发ArgumentException 引发异常。使用结构时,ValueType.Equals 进行的逐字节比较会导致两个调用都尝试添加相同的键。

【讨论】:

  • +1 表示EqualityComparer&lt;TKey&gt;.Default。这是正确的答案。
【解决方案2】:
  1. new object() == new object()false,因为引用类型具有引用相等性且两个实例不是同一个引用

  2. new int() == new int()true,因为值类型具有值相等性,并且两个默认整数的值是相同的值。请注意,如果您的结构中具有递增的引用类型或默认值,则默认值对于结构的比较也可能不相等。

如果您不喜欢默认的相等行为,您可以覆盖 EqualsGetHashCode 方法以及结构和类的相等运算符。

此外,如果您想要一种安全的方式来设置字典值,无论如何,您都可以执行dictionary[key] = value;,这将添加新值或使用相同的键更新旧值。

更新

@280Z28 发布了a comment,指出这个答案可能会产生误导,我承认并想解决这个问题。重要的是要知道:

  1. 默认情况下,引用类型的Equals(object obj) 方法和== 运算符在后台调用object.ReferenceEquals(this, obj)

  2. 最终需要重写运算符和实例方法以传播行为。 (例如,更改Equals 实现不会影响== 实现,除非显式添加嵌套调用)。

  3. 所有默认的 .NET 泛型集合都使用 IEqualityComparer&lt;T&gt; 实现来确定相等性(不是实例方法)。 IEqualityComparer&lt;T&gt; 可能(并且经常这样做)在其实现中调用实例方法,但这不是您可以指望的。使用的IEqualityComparer&lt;T&gt; 实现有两个可能的来源:

    1. 您可以在构造函数中显式提供它。

    2. 它将自动从EqualityComparer&lt;T&gt;.Default 检索(默认情况下)。如果你想全局配置默认的IEqualityComparer&lt;T&gt;EqualityComparer&lt;T&gt;.Default可以访问,你可以使用Undefault(在GitHub上)。

【讨论】:

  • 您的答案具有误导性,因为Dictionary&lt;TKey, TValue&gt; 使用EqualityComparer&lt;T&gt;.Default 进行比较,并且该比较器不使用运算符== 进行比较。运营商== 与此问题完全无关。
  • @280Z28,这是一个很好的观点 - 我更新了我的答案来解决这个问题。
  • 问题问为什么Dictionary&lt;TKey, TValue&gt; 在用作键时会以不同的方式处理类和结构。您的回复讨论了 == 运算符并提到覆盖 EqualsGetHashCode 以更改相等行为。第一部分无关紧要,第二部分仅在某些时候是正确的,并且对原始问题中描述的情况都没有给出任何解释。更新似乎解决了我的评论,好像我说你是不正确,而实际上我只是说你是离题
  • @280Z28 OP 要求详细解释他的行为。我将这个问题解释为表明 OP 对 .NET 平等存在误解,而字典键的问题只是这种误解的一种特殊表现。所以,我试图以我认为最有帮助的方式回答这个问题。我想您可以将更广泛的概括视为部分“离题”,但这是我回答的理由。
【解决方案3】:

通常有三种好的字典键类型:可变类对象的身份、不可变类对象的值或结构的值。请注意,具有公开公共字段的结构与那些没有公开字段的结构一样适合用作字典键,因为存储在字典中的结构副本将更改的唯一方法是读取、修改和写入结构背部。相比之下,具有暴露可变属性的类通常会生成糟糕的字典键,除非希望键入对象的身份,而不是对象的内容。

为了将一个类型用作字典键,它的EqualsGetHashCode 方法必须具有所需的语义,否则Dictionary 的构造函数必须被赋予一个IEqualityComparer&lt;T&gt;,它实现了所需的语义。类的默认 EqualsGetHashCode 方法将键入对象标识(如果希望键入可变对象的标识,则很有用;否则就没那么有用了)。值类型的默认 EqualsGetHashCode 方法通常会使用其成员的 EqualsGetHashCode 方法,但有一些皱纹:

  • 在结构上使用默认方法的代码通常比使用自定义编写方法的代码运行速度慢很多(有时是一个数量级)。

  • 仅包含原始类型的结构执行浮点比较的方式与包含其他类型的结构不同。例如,值 posZero=(1.0/(1.0/0.0)) 和 negZero=(-1.0/(1.0/0.0)) 将比较相等,但如果存储在仅包含原语的结构中,它们将比较不相等。请注意,即使他的值比较相等,它们在语义上也不相同,因为计算 1.0/posZero 将产生正无穷大,而 1.0/negZero 将产生负无穷大。

如果性能远非关键,则可以定义一个简单的结构 [只需声明适当的公共字段] 并将其放入 Dictionary 并使其表现为基于值的键。它不会非常有效,但它会起作用。字典通常会更有效地处理不可变类对象,但定义和使用不可变类对象有时比定义和使用“普通旧数据结构”要多。

【讨论】:

    【解决方案4】:

    因为struct 不像class 那样被引用。

    结构会创建自身的副本,而不是像类一样解析引用。

    因此,如果您尝试这样做:

    var a =  new MyStruct(){Prop = "Test"};
    var b =  new MyStruct(){Prop = "Test"};
    
    Console.WriteLine(a.Equals(b));
    

    //将打印为真

    如果你对一个类做同样的事情:

    var a =  new MyClass(){Prop = "Test"};
    var b =  new MyClass(){Prop = "Test"};
    
    Console.WriteLine(a.Equals(b));
    

    // 将打印错误! (假设你还没有实现一些比较功能) 因为引用不一样

    【讨论】:

      【解决方案5】:

      引用类型键(类)指向一个不同的引用; 值类型键(结构)指向相同的值。我认为这就是你得到例外的原因。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-12-24
        • 2017-04-21
        • 1970-01-01
        • 2013-12-03
        • 2013-07-25
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多