Доступ до змінної foreach у попередженні про закриття


86

Я отримую таке попередження:

Доступ до змінної foreach при закритті. Може мати різну поведінку при компіляції з різними версіями компілятора.

Ось як це виглядає в моєму редакторі:

вищезазначене повідомлення про помилку у спливаючому вікні наведення

Я знаю, як виправити це попередження, але я хочу знати, чому я отримаю це попередження?

Це стосується версії "CLR"? Це пов'язано з "ІЛ"?



1
TL; DR відповідь: додайте .ToList () або .ToArray () в кінці виразу запиту, і це позбудеться попередження
JoelFan

Відповіді:


136

У цьому попередженні є дві частини. Перший - це ...

Доступ до змінної foreach при закритті

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

Візьміть свій запит, зазначивши, що код, який ви витримали, є в основному розширеною формою того, що створює компілятор C # (до C # 5) для foreach1 :

Я [не] розумію, чому [наведене нижче] не є дійсним:

string s; while (enumerator.MoveNext()) { s = enumerator.Current; ...

Ну, це дійсно синтаксично. І якщо все , що ви робите в циклі використовує значення з sто все добре. Але закриття sпризведе до протиінтуїтивної поведінки. Погляньте на наступний код:

var countingActions = new List<Action>();

var numbers = from n in Enumerable.Range(1, 5)
              select n.ToString(CultureInfo.InvariantCulture);

using (var enumerator = numbers.GetEnumerator())
{
    string s;

    while (enumerator.MoveNext())
    {
        s = enumerator.Current;

        Console.WriteLine("Creating an action where s == {0}", s);
        Action action = () => Console.WriteLine("s == {0}", s);

        countingActions.Add(action);
    }
}

Якщо ви запустите цей код, ви отримаєте такий вивід консолі:

Creating an action where s == 1
Creating an action where s == 2
Creating an action where s == 3
Creating an action where s == 4
Creating an action where s == 5

Це те, чого ви очікуєте.

Щоб побачити щось, чого ви, мабуть, не очікуєте, запустіть такий код одразу після вищевказаного коду:

foreach (var action in countingActions)
    action();

Ви отримаєте такий вивід консолі:

s == 5
s == 5
s == 5
s == 5
s == 5

Чому? Оскільки ми створили п’ять функцій, які виконують абсолютно те саме: друкують значення s(яке ми закрили). Насправді це однакові функції ("Друк s", "Друк s", "Друк s" ...).

У той момент, коли ми переходимо до їх використання, вони роблять саме те, про що ми просимо: друкують значення s. Якщо ви подивитесь на останнє відоме значення s, то побачите, що воно 5. Отже, ми s == 5надрукуємось п’ять разів на консолі.

Що саме про те, про що ми просили, але, мабуть, не те, що хочемо.

Друга частина попередження ...

Може мати різну поведінку при компіляції з різними версіями компілятора.

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

Таким чином, наступний код дасть різні результати в різних версіях компілятора:

foreach (var n in numbers)
{
    Action action = () => Console.WriteLine("n == {0}", n);
    countingActions.Add(action);
}

Отже, він також видасть попередження R # :)

Мій перший фрагмент коду вище буде демонструвати однакову поведінку у всіх версіях компілятора, оскільки я не використовую foreach(скоріше, я розширив його так, як це роблять попередні компілятори C # 5).

Це для версії CLR?

Я не зовсім впевнений, що ви тут питаєте.

У дописі Еріка Ліпперта сказано, що зміна відбувається "в C # 5". Томуймовірно, вам потрібно націлитись на .NET 4.5 або пізнішу версію з компілятором C # 5 або пізнішої версії, щоб отримати нову поведінку, а все, що до цього отримує стару поведінку.

Але, щоб було зрозуміло, це функція компілятора, а не версія .NET Framework.

Чи є відповідність ІЛ?

Різний код створює різний ІЛ, тому в цьому сенсі є наслідки для генерованого ІЛ.

1 foreach - набагато більш поширена конструкція, ніж код, який ви опублікували у своєму коментарі. Проблема, як правило, виникає через використання foreach, а не через ручне перерахування. Ось чому зміни foreachв C # 5 допомагають запобігти цій проблемі, але не повністю.


7
Я насправді пробував цикл foreach на різних компіляторах, отримуючи різні результати, використовуючи одну і ту ж мету (.Net 3.5). Я використовував VS2010 (який, у свою чергу, використовує компілятор, пов'язаний з .net 4.0, на мою думку) та VS2012 (компілятор .net 4.5, на мою думку). В принципі це означає, що якщо ви використовуєте VS2013 та редагуєте проект, націлений на .Net 3.5, і будуєте його на сервері збірки, де встановлено дещо старіший фреймворк, ви можете бачити різні результати своєї програми на вашій машині та розгорнуту збірку.
Ykok

Хороша відповідь, але не впевнений, наскільки "foreach" актуальний. Хіба це не відбудеться з ручним переліченням чи навіть простим циклом for (int i = 0; i <collection.Size; i ++)? Здається, це проблема із закриттям, що виходить за межі сфери дії, або, точніше, проблема з тим, що люди розуміють, як закриття себе поводить, коли вони виходять за рамки того, що вони були визначені всередині.
Бред

Тут foreachвсе виходить із змісту запитання. Ви маєте рацію, що це може відбуватися різними, більш загальними способами.
ta.speot.is

1
Чому R # все ще попереджає мене, чи не читає цільовий фреймворк, який я встановив як 4,5.
Johnny_D

1
"Отже, мабуть, вам потрібно націлитись на .NET 4.5 або пізнішу версію". Це твердження не відповідає дійсності. Версія .NET, на яку ви націлюєтесь, ніяк не впливає, поведінка також змінюється в .NET 2.0, 3.5 та 4, якщо ви використовуєте для компіляції C # 5 (VS 2012 або новішої). Ось чому це попередження ви отримуєте лише у .NET 4.0 або раніше, якщо ви націлюєтеся на 4.5, ви не отримуєте попередження, оскільки ви не можете скомпілювати 4.5 на компіляторі C # 4 або старішому.
Скотт Чемберлен

12

Перша відповідь чудова, тому я подумав, що додам лише одну річ.

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

Якщо ти змінився

...Where(x => x.Name == property.Value)

до

...Where(x => x.Name == property.Value).ToList()

тоді reflectedModel буде присвоєно певний список у циклі foreach, тому ви не отримаєте попередження, оскільки перерахування точно відбуватиметься всередині циклу, а не поза ним.


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

Я прочитав прийняту відповідь і просто подумав "як це закриття, якщо воно не прив'язує змінні?" але тепер я розумію, що мова йде про те, коли відбувається оцінка, дякую!
Джером

Так, це очевидне універсальне рішення. Повільний, пам’ятний, але я думаю, що це справді на 100% працює у всіх випадках.
Al Kepp

8

Блок-сферична змінна повинна вирішити попередження.

foreach (var entry in entries)
{
   var en = entry; 
   var result = DoSomeAction(o => o.Action(en));
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.