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

发布于 2024-08-08 03:32:44 字数 539 浏览 1 评论 0原文

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;  
        }
    }
}

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

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;  
        }
    }
}

I completely understand why this exception is thrown when enumerating a list. It seems reasonable to expect that during enumeration the structure of the enumerated object does not change. However, does changing a value of a dictionary also changes its structure?
Specifically, the structure of its keys?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

£噩梦荏苒 2024-08-15 03:32:44

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

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

Because the values and keys are stored as a pair. There is not a separate structure for keys and values but instead a single structure which stores both as a set of pair values. When you change a value it necessitates changing the single underlying structure which contains both keys and values.

Does changing a value necessarily change the order of the underlying structure? No. But this is an implementation specific detail and the Dictionary<TKey,TValue> class, correctly, deemed not to reveal this by allowing modification of values as part of the API.

养猫人 2024-08-15 03:32:44

事实上,我明白你来自哪里。这里的大多数答案没有注意到的是您正在迭代键列表,而不是字典的项目本身。如果 .NET 框架程序员愿意,他们可以相当轻松地区分对字典结构所做的更改和对字典中值所做的更改。然而,即使人们迭代集合的键,他们通常最终还是会得到值。我怀疑 .NET 框架设计者认为,如果您要迭代这些值,您会想知道是否有某些东西正在从您的控制下改变它们,就像任何列表一样。要么是这样,要么他们没有认为这是一个足够重要的问题,值得进行区分一种变更和另一种变更所需的编程和维护。

I see where you're coming from, actually. What most of the answers here fail to notice is that you are iterating across the list of Keys, and not the Dictionary's Items themselves. If the .NET framework programmers wanted to, they could fairly easily differentiate between changes made to the structure of the dictionary and changes made to the values in the dictionary. Nevertheless, even when people iterate across the keys of a collection, they usually end up getting the values out anyway. I suspect the .NET framework designers figured that if you're iterating across those values, you'll want to know if something is changing them out from under you, just as much as with any List. Either that or they didn't see it as an important enough issue to merit the programming and maintenance that would be required to differentiate between one kind of change and another.

胡渣熟男 2024-08-15 03:32:44

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

Thanks to Vitaliy I went back and looked at the code some more and it looks like it is a specific implementation decision to disallow this (see snippet below). The Dictionary keeps a private value called verrsion which is incremented when changing the value of an existing item. When the enumerator is created it makes a note of the value at that time, then checks on each call to 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;
    }
}

I don't know of a reason why this would be necessary. You are still free to modify the properties of the value, just not assign it to a new value:

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
    }
  } 
}
赠我空喜 2024-08-15 03:32:44

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

It's possible that you've just inserted a new key into the dictionary, which would indeed change dictionary.Keys. Even though in this specific loop that will never happen, the [] operation in general can change the list of keys so this is flagged as a mutation.

写下不归期 2024-08-15 03:32:44

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

Indexer on Dictionary is potentially an operation that can change the structure of the collection, since it will add a new entry with such key if one doesn't exist already. This obviously isn't the case here, but I expect that Dictionary contract is deliberately kept simple in that all operations on the object are divided into "mutating" and "non-mutating", and all "mutating" operations invalidate enumerators, even if they don't actually change anything.

久夏青 2024-08-15 03:32:44

从文档(Dictionary.Item 属性):

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

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

From the documentation (Dictionary.Item Property):

You can also use the Item property to add new elements by setting the value of a key that does not exist in the Dictionary. When you set the property value, if the key is in the Dictionary, the value associated with that key is replaced by the assigned value. If the key is not in the Dictionary, the key and value are added to the dictionary. In contrast, the Add method does not modify existing elements.

So, as John indicates, there is no way for the framework to know that you haven't altered the contents of the list, so it assumes that you have.

金兰素衣 2024-08-15 03:32:44

对于那些对如何解决这个问题感兴趣的人,这里有一个可行的 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 方法,它将键复制到数组中。

For those interested in how to get around this problem, here's a modified version of Vitaliy's code that works:

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;
        }
    }
}

The answer is to copy the keys out into another enumerable then iterate over that collection. For some reason there is no KeyCollection.ToList method to make things easy. Instead you need to use the KeyCollection.CopyTo method, which copies the keys out into an array.

度的依靠╰つ 2024-08-15 03:32:44

简而言之,您正在修改字典集合,即使您实际上没有更改它的任何键。因此,更新后访问集合的下一次迭代会抛出异常,表明自上次访问以来该集合已被修改(并且正确地如此)。

要执行您想要的操作,您需要一种不同的方式来迭代元素,以便更改它们不会触发迭代器异常。

The short answer is that you are modifying the dictionary collection, even though you're not actually changing any of its keys. So the next iteration that accesses the collection after your update throws an exception that indicates that the collection was modified since your last access (and rightly so).

To do what you want, you need a different way of iterating through the elements, so that changing them won't trigger an iterator exception.

Saygoodbye 2024-08-15 03:32:44

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

事实上,您问题的答案是,您输入的代码实际上会产生编译器生成的([CompilerGenerate])状态机,该状态机允许迭代器维护集合状态以提供收益魔法。这就是为什么如果您不同步集合并且在一个线程中迭代并在另一个线程中进行操作,您将会遇到一些奇怪的事情。

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

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

It's because they designed .Net with the ability to iterate a collection in multiple threads. So you either gotta allow the iterator be multithreaded or prevent that and allow the modification of the collection during the iteration which would require limiting the object to be itereated in a single thread. Can't have both.

In fact the answer to your question is that the code you type in actually results in a compiler generated ([CompilerGenerated]) state machine that allows for iterators to maintain the collection state in order to provide the yield magic. Thats why if you dont synchronize your collections and you iterate in one thread and manipulate in another thread, you;ll get some funky shit going on.

Check out: http://csharpindepth.com/articles/chapter6/iteratorblockimplementation.aspx

Also: http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ConcurrentHashMap.html
"iterators are designed to be used by only one thread at a time."

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文