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


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

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


1465


источник


Ответы:


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

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

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

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

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

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

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

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


1268



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

Для меня наиболее убедительным аргументом является то, что наличие новой переменной на каждой итерации будет несовместимо с 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 # ,


174



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

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.

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


94



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


52



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

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

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

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

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


0