Почему переменная итерации в операторе C # foreach доступна только для чтения? - PullRequest
34 голосов
/ 22 апреля 2009

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

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

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

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

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

Ответы [ 6 ]

25 голосов
/ 23 апреля 2009

Давайте начнем с глупого, но наглядного примера:

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 запрещает вам переназначать указатель, используемый для итерации? Ясность с одной стороны - вы не хотите, чтобы люди, назначенные на это, думали, что изменили вашу позицию в цикле. Простота реализации для другого: переменная может скрывать некоторую внутреннюю логику, указывающую состояние текущего цикла.

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

14 голосов
/ 22 апреля 2009

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

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

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

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

10 голосов
/ 22 апреля 2009

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

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

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

2 голосов
/ 22 апреля 2009

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

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

2 голосов
/ 22 апреля 2009

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

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

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

1 голос
/ 22 апреля 2009

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

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

...