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

时间:2009-10-13 20:28:19

标签: c# dictionary enumeration invalidoperationexception

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;  

            }
        }
    }

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

9 个答案:

答案 0 :(得分:18)

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

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

答案 1 :(得分:7)

感谢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<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
    }
  } 
}

答案 2 :(得分:6)

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

答案 3 :(得分:5)

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

答案 4 :(得分:4)

Dictionary上的索引器可能是一个可以更改集合结构的操作,因为如果一个密钥已经不存在,它将添加一个带有这样的密钥的新条目。这显然不是这里的情况,但我希望Dictionary契约有意保持简单,因为对象上的所有操作都分为“变异”和“非变异”,并且所有“变异”操作都无效调查员,即使他们实际上没有改变任何东西。

答案 5 :(得分:1)

从文档(Dictionary.Item属性):

  

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

所以,正如约翰所指出的那样,框架无法知道你没有改变列表的内容,所以它假设你有。

答案 6 :(得分: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方法,该方法将密钥复制到数组中。

答案 7 :(得分:0)

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

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

答案 8 :(得分:0)

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

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

退房:http://csharpindepth.com/articles/chapter6/iteratorblockimplementation.aspx

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