Programming

C #의 루프에서 변수를 캡처

procodes 2020. 5. 10. 11:41
반응형

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을 출력 할 것으로 예상하지만 실제로는 5를 10으로 출력합니다.

하나의 캡처 변수를 참조하는 모든 조치 때문인 것으로 보입니다. 결과적으로, 호출 될 때 모두 동일한 출력을 갖습니다.

각 조치 인스턴스에 자체 캡처 변수가 있도록이 한계를 극복 할 수있는 방법이 있습니까?


예-루프 내부에서 변수의 사본을 가져옵니다.

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

이에 대한 자세한 내용은 C # 3.0 사양의 7.14.4.2 섹션을 참조 하십시오. 클로저에 대한 기사 에도 더 많은 예제가 있습니다.


나는 당신이 겪고있는 것이 Closure http://en.wikipedia.org/wiki/Closure_(computer_science) 라고 알려져 있습니다. 람 바에는 함수 외부에서 범위가 지정된 변수에 대한 참조가 있습니다. 람다는 호출 할 때까지 해석되지 않으며 일단 실행되면 변수가 실행 시간에 갖는 값을 얻습니다.


배후에서 컴파일러는 메소드 호출의 클로저를 나타내는 클래스를 생성합니다. 루프의 각 반복마다 클로저 클래스의 단일 인스턴스를 사용합니다. 코드는 다음과 같이 표시되어 버그가 발생하는 이유를 더 쉽게 확인할 수 있습니다.

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

This isn't actually the compiled code from your sample, but I've examined my own code and this looks very much like what the compiler would actually generate.


The way around this is to store the value you need in a proxy variable, and have that variable get captured.

I.E.

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

Yes you need to scope variable within the loop and pass it to the lambda that way:

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

The same situation is happening in multi-threading (C#, .NET 4.0].

See the following code:

Purpose is to print 1,2,3,4,5 in order.

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

The output is interesting! (It might be like 21334...)

The only solution is to use local variables.

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

This has nothing to do with loops.

This behavior is triggered because you use a lambda expression () => variable * 2 where the outer scoped variable not actually defined in the lambda's inner scope.

Lambda expressions (in C#3+, as well as anonymous methods in C#2) still create actual methods. Passing variables to these methods involve some dilemmas (pass by value? pass by reference? C# goes with by reference - but this opens another problem where the reference can outlive the actual variable). What C# does to resolve all these dilemmas is to create a new helper class ("closure") with fields corresponding to the local variables used in the lambda expressions, and methods corresponding to the actual lambda methods. Any changes to variable in your code is actually translated to change in that ClosureClass.variable

So your while loop keeps updating the ClosureClass.variable until it reaches 10, then you for loops executes the actions, which all operate on the same ClosureClass.variable.

To get your expected result, you need to create a separation between the loop variable, and the variable that is being closured. You can do this by introducing another variable, i.e.:

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

You could also move the closure to another method to create this separation:

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

You can implement Mult as a lambda expression (implicit closure)

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

or with an actual helper class:

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

In any case, "Closures" are NOT a concept related to loops, but rather to anonymous methods / lambda expressions use of local scoped variables - although some incautious use of loops demonstrate closures traps.


It is called the closure problem, simply use a copy variable, and it's done.

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

참고URL : https://stackoverflow.com/questions/271440/captured-variable-in-a-loop-in-c-sharp

반응형