Почему мы не можем изменить значения словаря при перечислении его ключей? - PullRequest
26 голосов
/ 14 октября 2009
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;  

            }
        }
    }

Я полностью понимаю, почему это исключение выдается при перечислении списка - кажется разумным ожидать, что во время перечисления структура перечисляемого объекта не изменится. Однако - меняет ли значение словаря его структуру ? Конкретно структура его ключей?

Ответы [ 9 ]

18 голосов
/ 14 октября 2009

Поскольку значения и ключи хранятся в виде пары. Не существует отдельной структуры для ключей и значений, а есть отдельная структура, которая хранит оба в виде набора парных значений. Когда вы изменяете значение, это требует изменения единой базовой структуры, которая содержит и ключи, и значения.

Обязательно ли изменение значения меняет порядок базовой структуры? Нет. Но это специфическая для реализации деталь, и класс Dictionary<TKey,TValue>, правильно, как полагают, не раскрывает этого, разрешая модификацию значений как часть API.

7 голосов
/ 14 октября 2009

Благодаря Виталию я вернулся и еще раз просмотрел код, и похоже, что это конкретное решение по реализации, чтобы запретить это (см. Фрагмент ниже). Словарь сохраняет частное значение 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
    }
  } 
}
6 голосов
/ 14 октября 2009

Я вижу, откуда ты, на самом деле. Что большинство ответов здесь не замечают, так это то, что вы перебираете список ключей, а не сами элементы словаря. Если бы программисты .NET Framework хотели, они могли бы довольно легко различить изменения, внесенные в структуру словаря, и изменения, внесенные в значения в словаре. Тем не менее, даже когда люди перебирают ключи в коллекции, они в любом случае обычно получают значения. Я подозреваю, что разработчики .NET Framework решили, что если вы выполняете итерации по этим значениям, вы захотите узнать, что-то меняет их из-под вас, так же, как и в любом другом List. Либо это, либо они не считают это достаточно важной проблемой, чтобы заслужить программирование и обслуживание, которые потребуются для проведения различия между одним видом изменений и другим.

5 голосов
/ 14 октября 2009

Возможно, вы только что вставили новый словарь в словарь, который действительно изменит dictionary.Keys. Несмотря на то, что в этом конкретном цикле этого никогда не произойдет, операция [] в целом может изменить список ключей, так что это помечается как мутация.

4 голосов
/ 14 октября 2009

Индексатор на Dictionary потенциально может быть операцией, которая может изменить структуру коллекции, так как он добавит новую запись с таким ключом, если он еще не существует. Это, очевидно, здесь не так, но я ожидаю, что контракт Dictionary намеренно остается простым в том смысле, что все операции над объектом делятся на «мутирующие» и «не мутирующие», а все «мутирующие» операции делают недействительными счетчики даже если они на самом деле ничего не меняют.

1 голос
/ 26 марта 2013

Для тех, кто интересуется, как обойти эту проблему, вот модифицированная версия кода Виталия, которая работает:

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, который копирует ключи в массив.

1 голос
/ 14 октября 2009

Из документации (свойство Dictionary.Item):

Вы также можете использовать свойство Item для добавления новых элементов, задав значение ключа, которого нет в Словаре. Когда вы устанавливаете значение свойства, если ключ находится в Словаре, значение, связанное с этим ключом, заменяется назначенным значением. Если ключ отсутствует в словаре, ключ и значение добавляются в словарь. Напротив, метод Add не изменяет существующие элементы.

Итак, как указывает Джон, у фреймворка нет возможности узнать, что вы не изменили содержимое списка, поэтому предполагается, что у вас есть.

0 голосов
/ 26 июня 2014

Это потому, что они разработали .Net с возможностью повторять коллекцию в нескольких потоках. Таким образом, вы должны либо разрешить многопоточность итератора, либо запретить это и разрешить модификацию коллекции во время итерации, которая потребует ограничения итераций объекта в одном потоке. Не может иметь оба.

На самом деле ответ на ваш вопрос заключается в том, что код, который вы вводите, на самом деле приводит к конечному автомату, сгенерированному компилятором ([CompilerGenerated]), который позволяет итераторам поддерживать состояние коллекции для обеспечения магии выхода. Вот почему, если вы не синхронизируете свои коллекции, итерируете в одном потоке и манипулируете в другом потоке, у вас будет какая-то шутка.

Выезд: http://csharpindepth.com/articles/chapter6/iteratorblockimplementation.aspx

Также: http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ConcurrentHashMap.html «Итераторы предназначены для использования только одним потоком за раз.»

0 голосов
/ 14 октября 2009

Короткий ответ: вы изменяете коллекцию словаря, даже если вы не меняете ни один из ее ключей. Поэтому следующая итерация, которая обращается к коллекции после вашего обновления, выдает исключение, которое указывает, что коллекция была изменена с момента вашего последнего доступа (и это правильно).

Чтобы сделать то, что вы хотите, вам нужен другой способ итерации элементов, чтобы их изменение не вызывало исключение итератора.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...