使用类和结构体作为字典键的区别

38

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

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是什么。能否补充一下这部分代码? - Rotem
你没有展示你获取实例的方式,但无论如何,对于你打算用作键的类,你需要重写GetHashCode和Equals方法。 - Anthony Pegram
1
实例定义已经添加。谢谢! - Kyle Baran
5个回答

80

Dictionary<TKey, TValue>使用IEqualityComparer<TKey>来比较键。如果在构造字典时没有明确指定比较器,则会使用EqualityComparer<TKey>.Default

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

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

如果当前实例和obj的字段中没有任何引用类型,则Equals方法对内存中的两个对象执行逐字节比较。否则,它使用反射来比较obj和此实例的相应字段。

异常发生是因为 IDictionary<TKey, TValue>.Add 抛出一个 ArgumentException,如果“字典中已经存在具有相同键的元素”。 在使用结构体时,由 ValueType.Equals 执行的逐字节比较导致两个调用都试图添加相同的键。

6
+1 表示支持使用 EqualityComparer<TKey>.Default。这是正确答案。 - Jim D'Angelo

19
  1. new object() == new object()false,因为引用类型具有引用相等性,两个实例不是同一个引用。

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

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

此外,如果您想安全地设置字典值,则可以使用dictionary[key] = value;来添加新值或更新具有相同密钥的旧值。

更新

@280Z28 发布了一条评论,指出这个答案可能会误导读者,我认识到并希望解决。重要的是要知道:

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

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

  • 所有默认的.NET泛型集合都使用一个 IEqualityComparer<T> 实现来确定相等性(而不是实例方法)。IEqualityComparer<T> 可以(并且经常会)在其实现中调用实例方法,但这并不是你可以计算的东西。有两个可能的来源可以使用 IEqualityComparer<T> 实现:

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

    2. 默认情况下,它将自动从 EqualityComparer<T>.Default 中检索(默认情况下)。如果您想要配置全局默认的 IEqualityComparer<T>,该 IEqualityComparer<T> 是通过 EqualityComparer<T>.Default 访问的,您可以使用Undefault(在 GitHub 上)。


  • 9
    你的回答有误导性,因为 Dictionary<TKey, TValue> 在比较时使用了 EqualityComparer<T>.Default,而该比较器并不使用操作符 == 进行比较。操作符 == 对于这个问题完全没有意义。 - Sam Harwell
    2
    @280Z28,这是一个很好的观点 - 我更新了我的答案来解决这个问题。 - smartcaveman
    问题是为什么在使用类和结构体作为键时,Dictionary<TKey, TValue>会有不同的处理方式。你的回答讨论了==运算符,并提到重写EqualsGetHashCode来改变相等性行为。其中第一部分与问题无关,第二部分只有在某些情况下才正确,并且两者都没有对原始问题描述的情况进行任何解释。更新似乎是在回应我的评论,好像我说你是“错误”的,而实际上我只是说你“离题”了。 - Sam Harwell
    3
    @280Z28询问了有关行为的详细解释。我理解这个问题是对.NET平等性的一般误解的表达,而字典键的问题只是该误解的特定表现。因此,我试图以最有帮助的方式回答这个问题。我认为你可以将更广泛的概括部分视为“离题”,但这是我回答的理由。 - smartcaveman

    7
    通常有三种适合作为字典键的类型:可变类对象的身份标识、不可变类对象的值或结构的值。请注意,具有公开字段的结构和没有公开字段的结构一样适合用作字典键,因为存储在字典中的结构副本只有在读取、修改并写回结构时才会更改。相比之下,在公开可变属性的类通常不适合作为字典键,除非希望根据对象的身份而不是其内容来进行键控。
    若要将类型用作字典键,必须使其Equals和GetHashCode方法具有所需的语义,否则必须给Dictionary的构造函数提供实现所需语义的IEqualityComparer。 对于类来说,默认的Equals和GetHashCode方法将基于对象身份进行键控(如果想要对可变对象进行身份标识,则很有用;否则就不太有用)。 对于值类型来说,默认的Equals和GetHashCode方法通常将基于其成员的Equals和GetHashCode方法进行键控,但还有一些问题:
    使用结构上的默认方法的代码通常会运行得比使用自定义方法的代码慢得多(有时会慢一个数量级)。
    仅包含原始类型的结构将以不同于包含其他类型的结构的方式执行浮点比较。 例如,值posZero =(1.0 /(1.0 / 0.0))和negZero =(-1.0 /(1.0 / 0.0))将都相等,但如果存储在仅包含原始类型的结构中,则它们将不相等。请注意,即使这些值相等,它们从语义上讲也不相同,因为计算1.0 / posZero将产生正无穷大,而1.0 / negZero将产生负无穷大。
    如果性能远非关键,可以定义一个简单的结构[只需声明适当的公共字段]并将其放入字典中,使其行为类似基于值的键。 它不会非常有效,但它会工作。 对于不可变类对象,字典通常会处理得更有效率,但是定义和使用不可变类对象有时可能比定义和使用"平凡的老数据结构"更费力。

    4

    因为structclass不同。

    struct会创建一个自身的副本,而不是像class那样解析引用。

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

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

    //将打印true

    如果您使用类执行相同的操作:

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

    // 将会输出 false!(假设您没有实现某些比较函数) 因为引用不相同


    1
    参考类型(类)键指向独立的引用;值类型(结构体)键指向相同的值。我认为这就是你得到异常的原因。

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接