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 ? 具体来说,它的键的结构?
答案 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)
答案 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;