Чи є причина повторного використання змінної C # в передбаченні?


1684

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


4
Що не так String s; foreach (s in strings) { ... }?
Бред Крісті

5
@BradChristie в OP не дуже йдеться, foreachале про ламда-вирази, що призводить до подібного коду, як показано в ОП ...
Yahia

22
@BradChristie: Це компілюється? ( Помилка: тип і ідентифікатор обоє необхідні в заяві foreach для мене)
Остін Салонен

32
@JakobBotschNielsen: це закрита зовнішня місцевість лямбда; чому ти припускаєш, що це взагалі буде на стеці? Термін експлуатації довший, ніж рамка стека !
Ерік Ліпперт

3
@EricLippert: Я розгублений. Я розумію, що лямбда фіксує посилання на змінну foreach (яка внутрішньо оголошена поза циклом), і тому ви в кінцевому підсумку порівнюєте її з кінцевим значенням; що я отримую. Що я не розумію, - це те, як оголошення змінної всередині циклу взагалі змінить значення. З точки зору компілятора-письменника, я виділяю лише один посилання рядка (var 's') на стеку незалежно від того, чи є декларація всередині або зовні циклу; Я, звичайно, не хотів би висувати нове посилання на стек кожну ітерацію!
Ентоні

Відповіді:


1407

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

Ваша критика цілком виправдана.

Я детально обговорюю цю проблему тут:

Закриття змінної циклу вважається шкідливим

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

Лист. Специфікація C # 1.0 насправді не говорить про те, чи була змінна циклу всередині або поза тілом циклу, оскільки вона не мала помітних змін. Коли в C # 2.0 було введено семантику закриття, було зроблено вибір, щоб поставити змінну циклу поза циклом, що відповідає циклу "для".

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

forЦикл не буде змінений, і зміна не буде «назад перенести» в попередні версії C #. Тому слід продовжувати бути обережними, використовуючи цю фразу.


177
Ми насправді підштовхували цю зміну в C # 3 і C # 4. Коли ми розробили C # 3, ми зрозуміли, що проблема (яка вже існувала в C # 2) буде загострюватися, оскільки буде так багато лямбда (і запитів розуміння, які є лямбдазами в маскуванні) в петлях переднього плану завдяки LINQ. Я шкодую, що ми дочекалися, коли проблема стане досить поганою, щоб вимагати її виправити так пізно, а не виправляти її в C # 3.
Ерік Ліпперт

75
А тепер нам доведеться пам’ятати, що foreachце «безпечно», але forце не так.
леппі

22
@michielvoo: Зміна вривається в тому сенсі, що вона не є сумісною назад. Новий код не працює належним чином при використанні старшого компілятора.
леппі

41
@Benjol: Ні, тому ми готові прийняти це. Джон Скіт вказав мені на важливий сценарій зміни зламу, який полягає в тому, що хтось пише код у C # 5, тестує його, а потім ділиться ним з людьми, які все ще використовують C # 4, які потім наївно вважають, що це правильно. Сподіваємось, кількість людей, постраждалих від такого сценарію, невелика.
Ерік Ліпперт

29
В сторону ReSharper завжди сприймав це і повідомляє про це як "доступ до модифікованого закриття". Потім, натиснувши Alt + Enter, він навіть автоматично виправить ваш код. jetbrains.com/resharper
Майк Чемберлен

191

Те, що ви запитуєте, докладно висвітлює Ерік Ліпперт у своєму дописі на блозі Закриття змінної циклу, що вважається шкідливою, та її продовження.

Для мене найбільш переконливим аргументом є те, що наявність нової змінної в кожній ітерації було б невідповідно 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 # .


18
Зрештою, те, що люди насправді хочуть, коли це пишуть, не має декількох змінних, це перекрити значення . І важко придумати придатний для цього синтаксис у загальному випадку.
Випадково832

1
Так, закрити значення не можна, однак є дуже простий спосіб вирішення, який я щойно відредагував, щоб відповісти включив.
Криз

6
Це занадто погано закриття в C # близько над посиланнями. Якщо вони закриті за значеннями за замовчуванням, ми можемо легко вказати закриття над змінними, а не з ref.
Шон У

2
@Krizz, це випадок, коли вимушена консистенція шкідливіша, ніж непослідовність. Він повинен "просто працювати", як очікують люди, і, очевидно, люди очікують чогось іншого при використанні функції foreach на відміну від циклу for, враховуючи кількість людей, які зіткнулися з проблемами, перш ніж ми дізналися про доступ до модифікованого питання про закриття (наприклад, я) .
Енді

2
@ Random832 не знає про C #, але в загальному LISP є синтаксис для цього, і він вважає, що будь-яка мова зі змінними змінними та закриттями (ні, мусить ) мати її теж. Ми або закриваємо посилання на місце, що змінюється, або значення, яке воно має в даний момент часу (створення закриття). Тут обговорюються подібні матеріали в Python та Scheme ( cutдля посилань / змін та cuteзбереження оцінених значень у частково оцінених закриттях).
Буде Несс

103

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

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.
}        

Якщо у вас є така звичка, ви можете її уникнути в дуже рідкісному випадку, який ви насправді мали намір прив’язати до зовнішніх областей. Якщо чесно, я не думаю, що я цього робив.


24
Ось типовий спосіб спасибі за внесок. Resharper досить розумний, щоб розпізнати цю модель і довести її до вашої уваги, що приємно. Я не був укушений за цією схемою деякий час, але оскільки це, за словами Еріка Ліпперта, "єдиний найпоширеніший звіт про помилки, який ми отримуємо", мені було цікаво дізнатися, чому більше, ніж як цього уникнути .
Стриптинг-воїн

62

У C # 5.0 ця проблема виправлена, і ви можете закрити змінні циклу та отримати очікувані результати.

Специфікація мови говорить:

8.8.4 Заява 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. )


1
Чому ця рання версія C # оголошена v всередині циклу while? msdn.microsoft.com/en-GB/library/aa664754.aspx
colinfang

4
@colinfang Не забудьте прочитати відповідь Еріка : специфікація C # 1.0 ( у вашому посиланні ми говоримо про VS 2003, тобто C # 1.2 ) насправді не сказала, чи була змінна циклу всередині або поза тілом циклу, оскільки це не має помітних відмінностей . Коли в C # 2.0 було введено семантику закриття, було зроблено вибір, щоб поставити змінну циклу поза циклом, що відповідає циклу "для".
Паоло Моретті

1
Отже, ви говорите, що приклади в посиланні на той час не були остаточними специфікаціями?
colinfang

4
@colinfang Це були остаточні характеристики. Проблема полягає в тому, що ми говоримо про функцію (тобто закриття функції), яка була введена пізніше (з C # 2.0). Коли з'явився C # 2.0, вони вирішили поставити змінну циклу поза циклом. І ніж вони знову передумали з C # 5.0 :)
Паоло Моретті
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.