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

Я встретил интересный вопрос о 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. Однако на самом деле он выводит пять 10s.

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

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

7 ответов


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

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 для получения более подробной информации об этом, и my статья о закрытиях есть еще примеры.


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


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

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

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


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

Е. И.

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

да, вам нужно scope 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();

такая же ситуация происходит в многопоточности (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();
}

это не имеет ничего общего с петель.

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

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

таким образом, ваш цикл while продолжает обновлять 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;
}

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