Есть ли причина для повторного использования 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 ответов
Компилятор объявляет переменную таким образом, что она очень подвержена ошибкам, которые часто трудно найти и отладить, но при этом не дает ощутимых преимуществ.
Ваша критика полностью оправдана.
Я обсуждаю эту проблему подробно здесь:
Закрытие по переменной цикла считается вредным
Есть ли что-то, что вы можете сделать с циклами foreach таким образом, что вы не смогли бы, если бы они были скомпилированы с переменной внутренней области видимости? или это просто произвольный выбор, который был сделан до того, как анонимные методы и лямбда-выражения были доступны или распространены, и который с тех пор не пересматривался?
Последний. Спецификация C# 1.0 фактически не говорила, была ли переменная цикла внутри или снаружи тела цикла, так как она не имела заметного различия. Когда семантика замыкания была введена в C# 2.0, был сделан выбор поместить переменную цикла вне цикла, в соответствии с циклом "for".
Я думаю, что будет справедливо сказать, что все сожалеют об этом решении. Это одна из худших "ошибок" в C#, и мы собираемся предпринять серьезные изменения, чтобы исправить это. В C# 5 переменная цикла foreach будет логически находиться внутри тела цикла, и поэтому замыкания будут получать свежую копию каждый раз.
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 месяца, когда вы (или кто-либо еще) прочитаете этот код, человек не поймет, что это такое и как оно должно работать.