29
class Program
{
    static void Main(string[] args)
    {
        var dictionary = new Dictionary<string, int>()
        {
            {"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;  
        }
    }
}

我完全理解为什么在枚举列表时会抛出这个异常。期望在枚举期间枚举对象的结构不会改变似乎是合理的。但是,更改字典的是否也会更改其结构?具体来说,它的键的结构?

4

9 回答 9

20

因为值和键是成对存储的。键和值没有单独的结构,而是一个单独的结构,将两者存储为一组对值。当您更改一个值时,需要更改包含键和值的单个底层结构。

改变一个值是否必然改变底层结构的顺序?不,但这是一个特定于实现的细节,并且Dictionary<TKey,TValue>该类正确地认为不会通过允许修改值作为 API 的一部分来揭示这一点。

于 2009-10-13T20:30:27.970 回答
8

实际上,我知道你来自哪里。这里的大多数答案都没有注意到的是,您正在遍历键列表,而不是字典的项目本身。如果 .NET 框架程序员愿意,他们可以很容易地区分对字典结构所做的更改和对字典中的值所做的更改。然而,即使人们遍历集合的键,他们通常最终还是会得到值。我怀疑 .NET 框架的设计者认为,如果您要遍历这些值,您会想知道是否有什么东西正在从您下面改变它们,就像使用任何 List 一样。要么,要么他们没有

于 2009-10-13T20:55:16.050 回答
8

多亏了 Vitaliy,我回去查看了更多代码,看起来这是一个特定的实现决定,不允许这样做(见下面的代码片段)。Dictionary 保留一个名为 version 的私有值,当更改现有项目的值时,该值会递增。创建枚举器时,它会记下当时的值,然后检查对 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<string, int>("1",1);
    kvp.Value = 17;
    var dictionary = new Dictionary<string, IntWrapper>(){
      {"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
    }
  } 
}
于 2009-10-13T20:57:08.003 回答
5

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

于 2009-10-13T20:31:36.020 回答
5

Indexer onDictionary可能是一种可以更改集合结构的操作,因为如果该键不存在,它将添加具有此类键的新条目。这显然不是这里的情况,但我希望Dictionary合约故意保持简单,因为对对象的所有操作都分为“变异”和“非变异”,所有“变异”操作都使枚举数无效,即使它们实际上并没有改变任何东西。

于 2009-10-13T20:32:43.530 回答
1

从文档(Dictionary.Item 属性):

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

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

于 2009-10-13T20:35:43.883 回答
1

对于那些对如何解决这个问题感兴趣的人,这里是 Vitaliy 代码的修改版本:

class Program
{
    static void Main(string[] args)
    {
        var dictionary = new Dictionary<string, int>()
        {
            {"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 方法,该方法将键复制到一个数组中。

于 2013-03-25T23:35:44.913 回答
0

简短的回答是您正在修改字典集合,即使您实际上并没有更改它的任何键。因此,在您更新后访问该集合的下一次迭代会引发一个异常,该异常表明该集合自您上次访问以来已被修改(这是正确的)。

为了做你想做的事,你需要一种不同的方式来迭代元素,这样改变它们就不会触发迭代器异常。

于 2009-10-13T20:58:58.530 回答
0

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

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

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

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

于 2014-06-26T18:53:12.887 回答