Есть ли причина для повторного использования C # переменной в foreach? - PullRequest
1603 голосов
/ 17 января 2012

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

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

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

* 1009.* Как объяснено здесь , это происходит потому, что переменная 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 таким образом,не могли бы, если бы они были скомпилированы с внутренней переменной, или это просто произвольный выбор, который был сделан до того, как анонимные методы и лямбда-выражения были доступны или распространены, и который с тех пор не пересматривался?

Ответы [ 5 ]

1359 голосов
/ 17 января 2012

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

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

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

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

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

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

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

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

182 голосов
/ 17 января 2012

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

Для меня наиболее убедительным аргументом является то, что наличие новой переменной в каждой итерации будет несовместимо с циклом стиля for(;;).Ожидаете ли вы иметь новый int i в каждой итерации for (int i = 0; i < 10; 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 в C # .

100 голосов
/ 17 января 2012

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

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.

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

58 голосов
/ 03 сентября 2012

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

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

8.8.4 Оператор foreach

(...)

оператор foreach в форме

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, оно будет передано среди всех итераций, и его значение после цикла будет окончательное значение, 13, которое будет выводить вызов f. Вместо этого, поскольку каждая итерация имеет свою собственную переменную v, захваченный f в первой итерации будет продолжать хранить значение 7, что будет напечатано. ( Примечание: более ранние версии C # объявлено v вне цикла while. )

0 голосов
/ 15 июня 2018

На мой взгляд, это странный вопрос. Хорошо знать, как работает компилятор, но это только «полезно знать».

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

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

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

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

...