Захваченная переменная в цикле в C # - PullRequest
189 голосов
/ 07 ноября 2008

Я встретил интересный вопрос о C #. У меня есть код, как показано ниже.

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    actions.Add(() => variable * 2);
    ++ variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

Я ожидаю, что он выведет 0, 2, 4, 6, 8. Однако на самом деле он выдает пять 10 с.

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

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

Ответы [ 8 ]

169 голосов
/ 07 ноября 2008

Да - взять копию переменной внутри цикла:

while (variable < 5)
{
    int copy = variable;
    actions.Add(() => copy * 2);
    ++ variable;
}

Вы можете думать об этом, как будто компилятор C # создает «новую» локальную переменную каждый раз, когда попадает в объявление переменной. Фактически, он создаст соответствующие новые объекты замыкания, и он станет сложным (с точки зрения реализации), если вы обращаетесь к переменным в нескольких областях, но он работает:)

Обратите внимание, что более распространенным явлением этой проблемы является использование for или foreach:

for (int i=0; i < 10; i++) // Just one variable
foreach (string x in foo) // And again, despite how it reads out loud

Подробнее об этом см. В разделе 7.14.4.2 спецификации C # 3.0, а в моей статье о замыканиях есть и другие примеры.

19 голосов
/ 07 ноября 2008

Я полагаю, что вы испытываете что-то, известное как Закрытие http://en.wikipedia.org/wiki/Closure_(computer_science). У вашей лямбы есть ссылка на переменную, которая выходит за пределы самой функции. Ваша лямба не интерпретируется до тех пор, пока вы ее не вызовете, и, как только она появится, она получит значение, которое переменная имеет во время выполнения.

9 голосов
/ 29 марта 2013

За кулисами компилятор генерирует класс, который представляет замыкание для вашего вызова метода. Он использует этот единственный экземпляр класса замыкания для каждой итерации цикла. Код выглядит примерно так, что упрощает понимание причины ошибки:

void Main()
{
    List<Func<int>> actions = new List<Func<int>>();

    int variable = 0;

    var closure = new CompilerGeneratedClosure();

    Func<int> anonymousMethodAction = null;

    while (closure.variable < 5)
    {
        if(anonymousMethodAction == null)
            anonymousMethodAction = new Func<int>(closure.YourAnonymousMethod);

        //we're re-adding the same function 
        actions.Add(anonymousMethodAction);

        ++closure.variable;
    }

    foreach (var act in actions)
    {
        Console.WriteLine(act.Invoke());
    }
}

class CompilerGeneratedClosure
{
    public int variable;

    public int YourAnonymousMethod()
    {
        return this.variable * 2;
    }
}

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

7 голосов
/ 07 ноября 2008

Способ обойти это - сохранить нужное значение в прокси-переменной и получить эту переменную.

И.Е.

while( variable < 5 )
{
    int copy = variable;
    actions.Add( () => copy * 2 );
    ++variable;
}
4 голосов
/ 28 января 2011

Такая же ситуация происходит в многопоточности (C #, .NET 4.0].

См. Следующий код:

Цель - напечатать 1,2,3,4,5 по порядку.

for (int counter = 1; counter <= 5; counter++)
{
    new Thread (() => Console.Write (counter)).Start();
}

Вывод интересный! (Это может быть как 21334 ...)

Единственное решение - использовать локальные переменные.

for (int counter = 1; counter <= 5; counter++)
{
    int localVar= counter;
    new Thread (() => Console.Write (localVar)).Start();
}
4 голосов
/ 07 ноября 2008

Да, вам нужно охватить variable внутри цикла и передать его лямбде следующим образом:

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    int variable1 = variable;
    actions.Add(() => variable1 * 2);
    ++variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

Console.ReadLine();
3 голосов
/ 13 июня 2018

Это не имеет ничего общего с петлями.

Это поведение вызвано тем, что вы используете лямбда-выражение () => variable * 2, где внешняя область variable фактически не определена во внутренней области лямбды.

Лямбда-выражения (в C # 3 +, а также анонимные методы в C # 2) по-прежнему создают фактические методы. Передача переменных в эти методы сопряжена с некоторыми дилеммами (передача по значению, передача по ссылке, C # идет по ссылке, но это открывает еще одну проблему, когда ссылка может пережить действительную переменную). Что C # делает, чтобы разрешить все эти дилеммы, так это создать новый вспомогательный класс («замыкание») с полями, соответствующими локальным переменным, используемым в лямбда-выражениях, и методами, соответствующими фактическим лямбда-методам. Любые изменения в variable в вашем коде фактически переводятся как изменения в ClosureClass.variable

Таким образом, ваш цикл while продолжает обновлять ClosureClass.variable до тех пор, пока он не достигнет 10, тогда цикл for выполняет действия, которые все работают с одним и тем же ClosureClass.variable.

Чтобы получить ожидаемый результат, вам нужно создать разделение между переменной цикла и закрываемой переменной. Вы можете сделать это, введя другую переменную, т. Е .:

List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
    var t = variable; // now t will be closured (i.e. replaced by a field in the new class)
    actions.Add(() => t * 2);
    ++variable; // changing variable won't affect the closured variable t
}
foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

Вы также можете переместить замыкание на другой метод для создания этого разделения:

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    actions.Add(Mult(variable));
    ++variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

Вы можете реализовать Mult как лямбда-выражение (неявное замыкание)

static Func<int> Mult(int i)
{
    return () => i * 2;
}

или с фактическим классом помощника:

public class Helper
{
    public int _i;
    public Helper(int i)
    {
        _i = i;
    }
    public int Method()
    {
        return _i * 2;
    }
}

static Func<int> Mult(int i)
{
    Helper help = new Helper(i);
    return help.Method;
}

В любом случае, "Замыкания" - это НЕ концепция, связанная с циклами , а скорее с анонимными методами / лямбда-выражениями, использующими локальные переменные области действия - хотя некоторые неосторожные использования циклов демонстрируют ловушки замыканий.

0 голосов
/ 12 декабря 2018

Это называется проблемой закрытия, просто используйте переменную копирования, и все готово.

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    int i = variable;
    actions.Add(() => i * 2);
    ++ variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...