Доступ до змінної 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.