Есть ли причина повторного использования С# переменной в foreach?

При использовании лямбда-выражений или анонимных методов в С# мы должны опасаться доступа к модифицированной ловушке закрытия. Например:

foreach (var s in strings)
{
   query = query.Where(i => i.Prop == s); // access to modified closure
   ...
}

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

Как объясняется здесь, это происходит потому, что переменная s, объявленная в цикле foreach выше, переводится как это в компиляторе:

string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}

вместо этого:

while (enumerator.MoveNext())
{
   string s;
   s = enumerator.Current;
   ...
}

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

string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}
var finalString = s;

Однако переменные, определенные в цикле foreach, не могут использоваться вне цикла:

foreach(string s in strings)
{
}
var finalString = s; // won't work: you're outside the scope.

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

Есть ли что-то, что вы можете сделать с foreach, таким образом, что вы не могли бы, если бы они были скомпилированы с переменной с внутренней областью, или это просто произвольный выбор, который был сделан до анонимных методов и лямбда-выражений или общий, и который не был пересмотрен с тех пор?

Ответ 1

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

Ваша критика полностью оправдана.

Я подробно обсуждаю эту проблему здесь:

Закрытие переменной цикла считается вредным

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

Последний. Спецификация С# 1.0 фактически не указала, была ли переменная цикла внутри или снаружи тела цикла, поскольку она не делала заметной разницы. Когда семантика закрытия была введена в С# 2.0, был сделан выбор, чтобы поместить переменную цикла вне цикла, согласующуюся с циклом "для".

Я считаю справедливым сказать, что все сожалеют об этом решении. Это один из худших "gotchas" на С#, а мы намерены исправить это исправление. В С# 5 переменная цикла foreach будет логически находиться внутри тела цикла, и поэтому закрытие будет получать новую копию каждый раз.

Цикл for не будет изменен, и изменение не будет "обратно перенесено" на предыдущие версии С#. Поэтому вы должны быть осторожны при использовании этой идиомы.

Ответ 2

То, что вы просите, полностью покрывается Эриком Липпертом в его сообщении в блоге Закрытие переменной цикла, считающейся вредной, и ее продолжение.

Для меня самым убедительным аргументом является то, что наличие новой переменной в каждой итерации будет противоречить циклу стиля for(;;). Ожидаете ли вы, что на каждой итерации for (int i = 0; i < 10; i++) есть новый int i?

Наиболее распространенной проблемой с этим поведением является замыкание переменной итерации, и она имеет простой способ:

foreach (var s in strings)
{
    var s_for_closure = s;
    query = query.Where(i => i.Prop == s_for_closure); // access to modified closure

Мое сообщение в блоге об этой проблеме: Закрытие переменной foreach в С#.

Ответ 3

Будучи укушенным этим, у меня есть привычка включать локально определенные переменные в самый внутренний охват, который я использую для переноса на любое закрытие. В вашем примере:

foreach (var s in strings)
{
    query = query.Where(i => i.Prop == s); // access to modified closure

Я делаю:

foreach (var s in strings)
{
    string search = s;
    query = query.Where(i => i.Prop == search); // New definition ensures unique per iteration.

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

Ответ 4

В С# 5.0 эта проблема исправлена, и вы можете закрыть переменные цикла и получить ожидаемые результаты.

В спецификации языка указано:

8.8.4 Оператор foreach

(...)

A для любой формулировки формы

foreach (V v in x) embedded-statement

затем расширяется до:

{
  E e = ((C)(x)).GetEnumerator();
  try {
      while (e.MoveNext()) {
          V v = (V)(T)e.Current;
          embedded-statement
      }
  }
  finally {
      … // Dispose e
  }
}

(...)

Размещение v внутри цикла while важно для того, как оно захваченных любой анонимной функцией, происходящей в погруженное выражение. Например:

int[] values = { 7, 9, 13 };
Action f = null;
foreach (var value in values)
{
    if (f == null) f = () => Console.WriteLine("First value: " + value);
}
f();

Если v было объявлено вне цикла while, оно будет разделяться среди всех итераций, и его значение после цикла for будет конечное значение, 13, что и выводит вызов f. Вместо этого, поскольку каждая итерация имеет свою собственную переменную v, одна захваченный f в первой итерации, продолжит удерживать значение 7, что и будет напечатано. (Примечание: более ранние версии С# объявлен v вне цикла while.)

Ответ 5

По-моему, это странный вопрос. Хорошо знать, как работает компилятор, но это только "хорошо знать".

Если вы пишете код, который зависит от алгоритма компилятора, это плохая практика. И лучше переписать код, чтобы исключить эту зависимость.

Это хороший вопрос для собеседования. Но в реальной жизни я не сталкивался с какими-либо проблемами, которые я решил на собеседовании.

90% foreach использует для обработки каждого элемента коллекции (не для выбора или вычисления некоторых значений). иногда вам нужно вычислить некоторые значения внутри цикла, но это не очень хорошая практика для создания цикла BIG.

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