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

1480

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

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

  • 4
    Что не так с String s; foreach (s in strings) { ... } ?
  • 5
    @BradChristie ОП на самом деле говорит не о foreach а о лямда-выражениях, которые приводят к тому же коду, который показан ОП ...
Показать ещё 8 комментариев
Теги:
scope
foreach
lambda
anonymous-methods

5 ответов

1215
Лучший ответ

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

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

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

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

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

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

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

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

  • 0
    Таким образом, нет возможности синтаксиса закрывать значения? (Конечно, у этого есть проблема "никто не использовал бы это", потому что синтаксис для закрытия по переменным более естественен и не делает разницы в 90% случаев)
  • 7
    @ Random832: маловероятно. Мы, однако, рассматриваем возможность добавления в Roslyn статического анализатора, который определяет, записывается ли когда-либо замкнутая переменная после создания замыкания; если это не так, то мы можем закрыть значение, а не переменную.
Показать ещё 22 комментария
174

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

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

  • 18
    В конечном счете, то, что люди на самом деле хотят, когда они пишут, это не иметь несколько переменных, а закрывать их по значению . И в общем случае сложно придумать подходящий для этого синтаксис.
  • 1
    Да, по значению закрыть невозможно, однако есть очень простой обходной путь, который я только что отредактировал, чтобы включить в него мой ответ.
Показать ещё 8 комментариев
99

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

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.

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

  • 22
    Это типичный обходной путь. Спасибо за вклад. Решарпер достаточно умен, чтобы распознать этот паттерн и обратить на него внимание, что приятно. В течение некоторого времени меня не волновал этот шаблон, но, поскольку он, по словам Эрика Липперта, «единственный наиболее распространенный неверный отчет об ошибках, который мы получаем», мне было любопытно узнать, почему, а не как его избежать .
54

В С# 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.)

  • 1
    Почему эта ранняя версия C # объявила v внутри цикла while? msdn.microsoft.com/en-GB/library/aa664754.aspx
  • 4
    @colinfang Обязательно прочитайте ответ Эрика : спецификация C # 1.0 ( в вашей ссылке мы говорим о VS 2003, то есть C # 1.2 ) на самом деле не говорила, была ли переменная цикла внутри или вне тела цикла, так как она не делает видимой разницы , Когда семантика замыкания была введена в C # 2.0, был сделан выбор поместить переменную цикла вне цикла, в соответствии с циклом «for».
Показать ещё 2 комментария
0

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

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

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

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

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

  • 1
    «Если вы пишете код, который зависит от алгоритма компилятора, это плохая практика». Это не вопрос оптимизации компилятора. Речь идет о реальном поведении языка, от которого вы не можете избежать зависимости. Я признаю, что знание того, почему было задано такое поведение языка, является скорее академическим упражнением, но это, очевидно, законный вопрос, учитывая, что команда C # решила внести решительные изменения в язык, чтобы исправить его.
  • 0
    «Лучше использовать выражения LINQ для вычисления значений». Вы заметите, что я использую цикл для построения выражения LINQ. Да, использование Aggregate в первую очередь позволило бы избежать этой проблемы, но люди очень часто используют такие циклы. Я вижу вопросы, возникающие из-за проблем такого рода в JavaScript, все время возникающих в StackOverflow, и они так же были распространены в C # до того, как команда решила изменить это поведение языка.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню