Подтвердить что ты не робот

Почему мы не можем изменять значения словаря при перечислении его ключей?

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 up with that?

                dictionary[s] = 1;  

            }
        }
    }

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

4b9b3361

Ответ 1

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

Ответ 2

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

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

Ответ 3

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

Ответ 4

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

Ответ 5

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

Ответ 6

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

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

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

Ответ 7

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

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

Ответ 8

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

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

Ответ 9

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

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

Отъезд: http://csharpindepth.com/articles/chapter6/iteratorblockimplementation.aspx

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