为什么我们不能在枚举其键时更改字典的值?

class Program { static void Main(string[] args) { var dictionary = new Dictionary() { {"1", 1}, {"2", 2}, {"3", 3} }; foreach (var s in dictionary.Keys) { // Throws the "Collection was modified exception..." on the next iteration // What's up with that? dictionary[s] = 1; } } } 

我完全理解为什么在枚举列表时抛出此exception – 在枚举期间,枚举对象的结构不会改变似乎是合理的。 但是,更改字典的值会改变其结构吗? 具体来说,其键的结构?

实际上,我看到你来自哪里。 这里的大多数答案都没有注意到,你是在重复键列表,而不是字典的项目本身。 如果.NET框架程序员想要,他们可以相当容易地区分对字典结构所做的更改以及对字典中值的更改。 然而,即使人们遍历集合的密钥,他们通常最终也会获得价值。 我怀疑.NET框架设计师认为,如果你在迭代这些值,你会想知道是否有什么东西正在改变它们,就像任何List一样。 无论是那个还是他们都认为这不是一个足够重要的问题,值得进行编程和维护,以区分一种变化和另一种变化。

因为值和键存储为一对。 键和值没有单独的结构,而是单个结构,它将两者都存储为一对配对值。 更改值时,必须更改包含键和值的单个基础结构。

更改值是否必然会改变基础结构的顺序? 不。但这是一个特定于实现的细节,而Dictionary类正确地认为不允许通过允许修改值作为API的一部分来揭示这一点。

感谢Vitaliy,我回过头来看了一下代码,看起来它是一个特定的实现决定,不允许这样做(参见下面的代码片段)。 字典保留一个名为verrsion的私有值,在更改现有项的值时会增加该值。 创建枚举器时,它会记下当时的值,然后检查每次调用MoveNext。

 for (int i = this.buckets[index]; i >= 0; i = this.entries[i].next) { if ((this.entries[i].hashCode == num) && this.comparer.Equals(this.entries[i].key, key)) { if (add) { ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate); } this.entries[i].value = value; this.version++; return; } } 

我不知道为什么这是必要的。 您仍然可以自由修改值的属性,只是不将其分配给新值:

 public class IntWrapper { public IntWrapper(int v) { Value = v; } public int Value { get; set; } } class Program { static void Main(string[] args) { var kvp = new KeyValuePair("1",1); kvp.Value = 17; var dictionary = new Dictionary(){ {"1", new IntWrapper(1)}, {"2", new IntWrapper(2)}, {"3", new IntWrapper(3)} }; foreach (var s in dictionary.Keys) { dictionary[s].Value = 1; //OK dictionary[s] = new IntWrapper(1); // boom } } } 

您可能刚刚在字典中插入了一个新密钥,这确实会改变dictionary.Keys 。 即使在这个永远不会发生的特定循环中, []操作通常也可以更改密钥列表,因此将其标记为变异。

Dictionary上的Indexer可能是一个可以改变集合结构的操作,因为它将添加一个带有这样的密钥的新条目(如果还没有)。 这显然不是这里的情况,但我希望Dictionary合同有意保持简单,因为对象上的所有操作都分为“变异”和“非变异”,所有“变异”操作使枚举器无效,即使他们实际上没有改变任何东西。

从文档(Dictionary.Item属性):

您还可以使用Item属性通过设置Dictionary中不存在的键的值来添加新元素。 设置属性值时,如果键位于“词典”中,则与该键关联的值将替换为指定的值。 如果该键不在字典中,则键和值将添加到字典中。 相反,Add方法不会修改现有元素。

因此,正如John指出的那样,框架无法知道您没有更改列表的内容,因此它假设您已经改变了。

对于那些对如何解决这个问题感兴趣的人,这里有一个Vitaliy代码的修改版本,可以工作:

 class Program { static void Main(string[] args) { var dictionary = new Dictionary() { {"1", 1}, {"2", 2}, {"3", 3} }; string[] keyArray = new string[dictionary.Keys.Count]; dictionary.Keys.CopyTo(keyArray, 0); foreach (var s in keyArray) { dictionary[s] = 1; } } } 

答案是将密钥复制到另一个枚举中,然后迭代该集合。 由于某种原因,没有KeyCollection.ToList方法使事情变得简单。 相反,您需要使用KeyCollection.CopyTo方法,该方法将密钥复制到数组中。

简短的回答是您正在修改字典集合,即使您实际上并未更改其任何键。 因此,在更新后访问集合的下一次迭代会抛出一个exception,指示自上次访问以来集合已被修改(并且正确地如此)。

要做你想做的事,你需要一种不同的迭代遍历元素的方法,这样改变它们就不会触发迭代器exception。

这是因为他们设计了.Net,能够在多个线程中迭代集合。 所以你要么允许迭代器是multithreading的,要么阻止它,并允许在迭代期间修改集合,这需要限制在单个线程中迭代对象。 不能兼得。

事实上,您的问题的答案是您输入的代码实际上导致编译器生成([CompilerGenerated])状态机,允许迭代器维护集合状态以提供yield魔术。 这就是为什么如果你不同步你的集合,你迭代一个线程并在另一个线程中操作,你会得到一些时髦的狗屎。

查看: http : //csharpindepth.com/articles/chapter6/iteratorblockimplementation.aspx

另外: http : //docs.oracle.com/javase/7/docs/api/java/util/concurrent/ConcurrentHashMap.html “迭代器设计为一次只能由一个线程使用。”