Почему переменная итерации в операторе C# foreach доступна только для чтения?

Насколько я понимаю, переменная итерации C# foreach неизменяема.

Это означает, что я не могу изменить итератор следующим образом:

foreach (Position Location in Map)
{
     //We want to fudge the position to hide the exact coordinates
     Location = Location + Random();     //Compiler Error

     Plot(Location);
}

Я не могу изменить переменную итератора напрямую, и вместо этого я должен использовать цикл for

for (int i = 0; i < Map.Count; i++)
{
     Position Location = Map[i];
     Location = Location + Random();

     Plot(Location);        
     i = Location;
}

Исходя из фона C++, я рассматриваю foreach как альтернативу циклу for. Но с указанным выше ограничением я обычно прибегаю к циклу for.

Мне любопытно, в чем причина неизменяемости итератора?


Редактировать:

Этот вопрос больше связан с любопытством, а не с вопросом о кодировании. Я оценил ответы по кодированию, но я не могу пометить их как ответы.

Кроме того, приведенный выше пример был чрезмерно упрощен. Вот пример C++ того, что я хочу сделать:

// The game's rules: 
//   - The "Laser Of Death (tm)" moves around the game board from the
//     start area (index 0) until the end area (index BoardSize)
//   - If the Laser hits a teleporter, destroy that teleporter on the
//     board and move the Laser to the square where the teleporter 
//     points to
//   - If the Laser hits a player, deal 15 damage and stop the laser.

for (int i = 0; i < BoardSize; i++)
{
    if (GetItem(Board[i]) == Teleporter)
    {
        TeleportSquare = GetTeleportSquare(Board[i]);
        SetItem(Board[i], FreeSpace);
        i = TeleportSquare;
    }

    if (GetItem(Board[i]) == Player)
    {
        Player.Life -= 15;
        break;
    }
}

Я не могу сделать это в C# foreach, потому что итератор i неизменяем. Я думаю (поправьте меня, если я ошибаюсь), это специфично для дизайна foreach в языках.

Меня интересует, почему итератор foreach неизменен.


person MrValdez    schedule 22.04.2009    source источник
comment
Хорошая находка. Я немного удивлен, что никогда не замечал этого раньше.   -  person Matt Hamilton    schedule 22.04.2009
comment
На самом деле вы также не меняете элемент массива во втором примере.   -  person Jakob Christensen    schedule 22.04.2009
comment
@Jakob Якоб, меня не очень интересует изменение элемента массива. Меня больше интересует, почему итератор неизменяем. Думаю, мне нужно отредактировать вопрос, чтобы уточнить его.   -  person MrValdez    schedule 22.04.2009
comment
@Jakob Я исправлен, в моем втором примере отсутствовала строка. Спасибо за внимание.   -  person MrValdez    schedule 23.04.2009


Ответы (6)


Давайте начнем с глупого, но показательного примера:

Object o = 15;
o = "apples";

Ни в коем случае у нас не создается впечатление, что мы только что превратили число 15 в цепочку яблок. Мы знаем, что o — это просто указатель. Теперь давайте сделаем это в форме итератора.

int[] nums = { 15, 16, 17 };

foreach (Object o in nums) {
     o = "apples";
}

Опять же, это действительно ничего не дает. Или, по крайней мере, он ничего не выполнил бы при компиляции. Он определенно не вставит нашу строку в массив int — это не разрешено, и мы знаем, что o в любом случае — это просто указатель.

Возьмем ваш пример:

foreach (Position Location in Map)
{
     //We want to fudge the position to hide the exact coordinates
     Location = Location + Random();     //Compiler Error

     Plot(Location);
}

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

foreach (Position Location in Map)
{
     //We want to fudge the position to hide the exact coordinates
     Position Location2 = Location + Random();     //No more Error

     Plot(Location2);
}

Итак, почему Microsoft запрещает вам повторно назначать указатель, используемый для итерации? Во-первых, ясность — вы не хотите, чтобы люди, назначающие это, думали, что они изменили вашу позицию в цикле. Простота реализации для другого: переменная может скрывать некоторую внутреннюю логику, указывающую на состояние выполняемого цикла.

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

person tylerl    schedule 23.04.2009
comment
Тайлер, я могу ошибаться, но я не согласен с вашим утверждением, что Postion Location или Object O являются итераторами. Вместо этого они являются переменными итерации, с которыми работают итераторы. Если это действительно так, то я думаю, что ваши рассуждения могут быть немного искажены. Я согласен с тем, что изменение значения переменной итерации может вызвать проблемы с ясностью, но я бы хотел, чтобы Microsoft оставила это на усмотрение разработчика. Я нашел ряд случаев, когда было бы полезно изменить значение переменной итерации foreach. - person Anthony Gatlin; 07.10.2011
comment
@AnthonyGatlin Я не согласен с тем, что в этом случае они должны позволить разработчику решить. Запутанный код обходится очень дорого, и у Microsoft достаточно людей, которые пишут код на C#, и у них есть реальный стимул создать язык, который, насколько это возможно, предотвращает создание ошибок разработчиком. - person tylerl; 18.11.2011
comment
Переменная должна означать только одно. Хорошая концепция. Однако в моей голове было бы лучше, если бы это означало просто текущий элемент, а не ссылку или указатель. Или, в экземпляре операции Map[i], что именно то, что мы интуитивно ожидаем. - person cregox; 04.05.2012
comment
Я тоже не согласен с вами. foreach в С# убивает всю концепцию изменяемых итераторов. Я вынужден делать копию во время итерации с foreach и впоследствии назначать копию, что действительно неэффективно. Или отказаться от использования синтаксического сахара вроде foreach в пользу старых добрых циклов for(i;i‹n;i++) для этой цели. В C++ наш основанный на диапазоне for(auto& item: collection) позволяет нам создавать изменяемые итераторы. Единственным обходным путем является создание повторяющихся предметных классов и использование для них методов мутации. Определенно не весело, если мы хотим мутировать примитивы. - person Петър Петров; 27.01.2017
comment
foreach (Object o in nums) { - разве это не то же самое, что приведение List<T> в IList? Это вполне осуществимо. Однако назначение произвольного object через индексатор IList невозможно: если назначенное значение не соответствует строго типизированному типу элемента списка, будет выдано исключение. С o = "apples"; можно было бы обращаться так же. - person O. R. Mapper; 05.02.2017
comment
Кроме того, вы не хотите, чтобы люди, назначающие его, думали, что они изменили вашу позицию в цикле - как я указал в другом комментарии здесь, в настоящее время я не вижу никаких технических причин, почему переменная итерации в foreach петля не может так работать. - person O. R. Mapper; 05.02.2017

Если бы переменная была изменяемой, это могло бы создать неверное впечатление. Например:

string[] names = { "Jon", "Holly", "Tom", "Robin", "William" };

foreach (string name in names)
{
    name = name + " Skeet";
}

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

person Jon Skeet    schedule 22.04.2009
comment
Думаю, именно это и пытается проиллюстрировать Джон. Обратите внимание на Если. - person Brian Rasmussen; 22.04.2009
comment
Брайан, ваш комментарий был ответом на другой комментарий, который сейчас удален? - person Jon Skeet; 22.04.2009
comment
Вы когда-нибудь искали это в своей спецификации? Я предполагаю, что дизайнеры сделали значение только для чтения для ясности, как вы предлагаете, но было бы интересно узнать, были ли другие соображения! - person Jon Artus; 18.06.2010
comment
Это ты и твоя семья, Джон? - person Dio F; 14.02.2013
comment
Некоторые люди могут подумать, что это изменит содержимое массива. - Я не уверен, в чем здесь проблема. Очевидно, компилятор может скомпилировать любой доступ к тому, что выглядит как простая переменная итерации в исходном коде, во что-то вроде вызова метода получения/установки свойства, который может изменить базовый массив по мере необходимости. - person O. R. Mapper; 05.02.2017
comment
@O.R.Mapper: Это было бы очень неожиданным поведением, ИМО. И хотя это могло работать для массивов, это не могло работать для других форм foreach, что приводило к очень странному дизайну IMO. Я рад, что это так. - person Jon Skeet; 06.02.2017
comment
@JonSkeet: Это было бы очень неожиданным поведением, ИМО. - учитывая, что неудивительно, что свойства ведут себя одинаково, наши мнения на этот счет расходятся. В частности, я не уверен, что невозможность присваивания переменной итерации менее удивительна в, казалось бы, простых ситуациях (например, при итерации по массиву). И хотя это могло работать для массивов, это не могло работать для других форм foreach — компилятор допускает foreach для вещей, которые реализуют IEnumerable. Я не вижу причин, по которым он также не может разрешить присвоение переменной итерации внутри... - person O. R. Mapper; 06.02.2017
comment
... foreach при выполнении другого аналогичного условия (например, интерфейса IModifyableEnumerable). В конечном счете, однако, человек привыкает к тому, как работает язык. - person O. R. Mapper; 06.02.2017
comment
@ORMapper: свойства и локальные переменные действуют по-разному во всех отношениях. Вы предлагаете что-то изменить, чтобы один конкретный тип локальной переменной вел себя иначе, чем другие. Обратите внимание, что удивление от невозможности присвоить переменной итерации приводит к ошибке времени компиляции, тогда как удивление от того, что она компилируется, но ведет себя иначе, чем ожидалось (IMO), приведет к тонким ошибкам. - person Jon Skeet; 06.02.2017
comment
@JonSkeet: заставить локальную переменную одного типа вести себя иначе, чем другие - она ​​уже ведет себя по-разному с другими. Его нельзя назначить. Возможно, я предлагаю сделать так, чтобы локальные переменные этого типа вели себя более аналогично другим. - person O. R. Mapper; 06.02.2017
comment
@ORMapper: Но это более безопасная и простая разница, чем та, которую вы предлагали. В любом случае, я не думаю, что кто-то из нас сможет убедить другого. Я решительно поддерживаю языковой дизайн здесь. - person Jon Skeet; 06.02.2017
comment
@O.R.Mapper: с ref локальными переменными в C # 7 я вижу вариант использования для объявления переменной итерации как ref, а также для массивов. Это было бы разумно, но не просто делать это неявно, ИМО. - person Jon Skeet; 06.02.2017

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

using System;
using System.Collections.Generic;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {

            List<MyObject> colection =new List<MyObject>{ new MyObject{ id=1, Name="obj1" }, 
                                                    new MyObject{ id=2, Name="obj2"} };

            foreach (MyObject b in colection)
            {
             // b += 3;     //Doesn't work
                b.Add(3);   //Works
            }
        }

        class MyObject
        {
            public static MyObject operator +(MyObject b1, int b2)
            {
                return new MyObject { id = b1.id + b2, Name = b1.Name };
            }

          public void Add(int b2)
          {
              this.id += b2;
          }
            public string Name;
            public int id;
        }
    }
}

Я не знал об этом феномене, потому что всегда модифицировал объекты так, как я описал.

person majkinetor    schedule 22.04.2009

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

См. также этот вопрос: Как лучше всего изменить список в foreach

person Rik    schedule 22.04.2009
comment
Можете ли вы назвать хотя бы один конкретный пример такого побочного эффекта, с которым должен был бы справиться перечислитель (который не может возникнуть, когда переменная итерации неизменяема)? В противном случае этот ответ звучит крайне расплывчато. - person O. R. Mapper; 05.02.2017
comment
Я согласен с @ORMapper. Я могу понять, почему добавление/удаление элементов из коллекции — это плохо (должен ли foreach вернуться в цикл, чтобы получить доступ к тому, что вы только что вставили?), но обновление элементов в коллекции должно быть в порядке. - person David Klempfner; 18.09.2019

Оператор foreach работает с Enumerable. Отличие Enumerable в том, что он не знает о коллекции в целом, он почти слеп.

Это означает, что он не знает, что будет дальше, и вообще есть ли что-нибудь до следующего цикла итерации. Вот почему вы часто видите .ToList(), чтобы люди могли получить количество Enumerable.

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

person Ian    schedule 22.04.2009
comment
Но изменение переменной все равно не изменит перечислитель. Это просто локальная копия текущего значения. - person Jon Skeet; 22.04.2009
comment
Конечно, это зависит от того, является ли это ссылкой или типом значения? Но да, я понимаю, что вы смотрите на значение и, возможно, не настраиваете сам счетчик... хм - person Ian; 22.04.2009
comment
Конечно, это не веская причина не разрешать изменение переменной итератора. Компилятор уже жестко запрограммирован на поиск IEnumerable/IEnumerable<T>, чтобы решить, разрешать или нет цикл foreach в первую очередь. Если бы разработчики языка разрешили модификацию переменной итерации, компилятор мог бы принять такие модификации в тех случаях, когда цикл foreach используется для итерации по IList/IList<T>. - person O. R. Mapper; 05.02.2017
comment
@Ian, не имеет значения, это тип ссылки или значения. Если вы присваиваете новое значение переменной типа значения или новую ссылку на переменную ссылочного типа, в любом случае вы обновляете только эту локальную переменную, а не элемент в коллекции. - person David Klempfner; 18.09.2019

Условием работы с объектами IEnumerable является то, что базовая коллекция не должна изменяться, пока вы обращаетесь к ней с помощью Enumerable. Вы можете предположить, что объект Enumerable является моментальным снимком исходной коллекции. Поэтому, если вы попытаетесь изменить коллекцию во время перечисления, она вызовет исключение. Однако извлеченные объекты в Enumeration вовсе не являются неизменными.

Поскольку переменная, используемая в цикле foreach, является локальной для блока цикла, эта переменная недоступна вне блока.

person S M Kamran    schedule 22.04.2009