假设我有以下类和结构定义,并将它们作为字典对象中的键使用:
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");
它表示密钥已经存在,正如预期的那样,但仅适用于结构。该类将其视为两个单独的条目。我认为它与作为参考与价值的数据有关,但我想更详细地解释原因。
答案 0 :(得分:52)
Dictionary<TKey, TValue>
使用IEqualityComparer<TKey>
来比较密钥。如果在构造字典时没有明确指定比较器,则它将使用EqualityComparer<TKey>.Default
。
由于MyClass
和MyStruct
都没有实现IEquatable<T>
,因此默认的相等比较器会调用Object.Equals
和Object.GetHashCode
来比较实例。 MyClass
派生自Object
,因此实现将使用引用相等进行比较。另一方面,MyStruct
派生自System.ValueType
(所有结构的基类),因此它将使用ValueType.Equals
来比较实例。此方法的文档说明如下:
ValueType.Equals(Object)
方法会覆盖Object.Equals(Object)
,并为.NET Framework中的所有值类型提供值相等的默认实现。如果当前实例和
obj
的所有字段都不是引用类型,则Equals
方法会对内存中的两个对象执行逐字节比较。否则,它使用反射来比较obj
和此实例的相应字段。
发生异常是因为如果“[字典]中已存在”具有相同键的元素,IDictionary<TKey, TValue>.Add
将引发ArgumentException
。使用结构时,ValueType.Equals
进行的逐字节比较会导致两个调用都尝试添加相同的密钥。
答案 1 :(得分:12)
new object() == new object()
false ,因为引用类型具有引用相等性且两个实例不是相同的引用
new int() == new int()
true ,因为值类型具有值相等性,并且两个默认整数的值是相同的值。请注意,如果您的结构中具有增量的引用类型或默认值,则默认值可能不会比较结构的相等。
如果您不喜欢默认的相等行为,则可以覆盖Equals
和GetHashCode
方法以及结构和类的相等运算符。
此外,如果您想要一种安全的方式来设置词典值,您可以执行dictionary[key] = value;
,这将添加新值或使用相同的键更新旧值。
@280Z28发布了a comment,指出这个答案可能会产生误导,我承认并希望解决这个问题。知道这一点很重要:
默认情况下,引用类型为“Equals(object obj)
方法”和==
操作员调用object.ReferenceEquals(this, obj)
。
最终需要覆盖运算符和实例方法以传播行为。 (例如,更改Equals
实现不会影响==
实现,除非明确添加嵌套调用。)
所有默认的.NET泛型集合都使用IEqualityComparer<T>
实现来确定相等性(而不是实例方法)。 IEqualityComparer<T>
可能(通常会)在其实现中调用实例方法,但这不是您可以依赖的。使用IEqualityComparer<T>
实现有两种可能的来源:
您可以在构造函数中明确提供它。
将从EqualityComparer<T>.Default
自动检索(默认情况下)。如果要配置IEqualityComparer<T>
访问的全局默认EqualityComparer<T>.Default
,可以使用Undefault(在GitHub上)。
答案 2 :(得分:7)
通常有三种好的字典键类型:可变类对象的标识,不可变类对象的值或结构的值。请注意,具有公开公共字段的结构与不使用公共字段的结构一样适合用作字典键,因为如果结构被读出,修改和写入,则存储在字典中的结构的副本将改变的唯一方式是背部。相比之下,具有暴露的可变属性的类通常会产生糟糕的字典键,除非是希望键入对象的标识而不是其内容。
为了将类型用作字典键,其Equals
和GetHashCode
方法必须具有所需的语义,否则必须给出Dictionary
的构造函数一个IEqualityComparer<T>
,它实现了所需的语义。类的默认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将产生负无穷大。
如果性能不是很关键,那么可以定义一个简单的结构[简单地声明相应的公共字段]并将其抛入字典并使其表现为基于值的键。它不会非常有效,但它会起作用。字典通常会更有效地处理不可变类对象,但定义和使用不可变类对象有时比定义和使用“普通旧数据结构”更有用。
答案 3 :(得分: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));
//将打印错误! (假设你没有实现一些比较功能) 因为参考不一样
答案 4 :(得分:1)
引用类型键(类)指向不同的引用; 值类型键(struct)指向相同的值。我认为这就是你获得例外的原因。