Захваченная переменная в цикле в С#

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

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 секунд.

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

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

Ответы

Ответ 1

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

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

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

Обратите внимание, что более распространенным явлением этой проблемы является использование 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 спецификации С# 3.0, а также в моей статье о закрытии.

Ответ 2

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

Ответ 3

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

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

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

Ответ 4

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

т.е.

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

Ответ 5

Да, вам нужно обладать 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();

Ответ 6

То же самое происходит в многопоточном режиме (С#, .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();
}

Ответ 7

Это не имеет никакого отношения к циклам.

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

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

Таким образом, ваш цикл while обновляет ClosureClass.variable до ClosureClass.variable пор, пока он не достигнет 10, тогда вы для циклов выполняете действия, которые все работают на одном и том же 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;
}

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

Ответ 8

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

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());
}