36

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

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");

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

4

5 回答 5

71

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

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

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

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

发生异常是因为IDictionary<TKey, TValue>.Add抛出一个ArgumentExceptionif “[dictionary] 中已经存在具有相同键的元素”。使用结构时,逐字节比较 byValueType.Equals会导致两个调用都尝试添加相同的键。

于 2013-05-10T00:13:41.393 回答
16
  1. new object() == new object()false,因为引用类型具有引用相等性并且两个实例不是同一个引用

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

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

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

更新

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

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

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

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

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

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

于 2013-05-09T22:30:08.040 回答
7

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

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

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

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

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

于 2013-05-10T16:27:59.990 回答
4

因为 astruct不像 a 那样被引用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));

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

于 2013-05-09T22:30:11.163 回答
1

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

于 2013-05-09T22:29:53.493 回答