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

5 1511

5 ответов:

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

ваша критика вполне оправдана.

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

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

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

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

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

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

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

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

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

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.

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

в 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.)

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

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

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

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

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