[C#] 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 섹션을 참조 하십시오. 클로저에 대한 기사 에도 더 많은 예제가 있습니다.

C # 5 컴파일러 이후 (이전 버전의 C #을 지정할 때도)의 동작이 foreach변경되어 더 이상 로컬 사본을 만들 필요가 없습니다. 자세한 내용은 이 답변 을 참조하십시오.


답변

나는 당신이 겪고있는 것이 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;
    }
}

이것은 실제로 샘플에서 컴파일 된 코드는 아니지만 내 코드를 검사했으며 컴파일러가 실제로 생성하는 것과 매우 유사합니다.


답변

이를 해결하는 방법은 프록시 변수에 필요한 값을 저장하고 해당 변수를 캡처하는 것입니다.

IE

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


답변

이것은 루프와 관련이 없습니다.

() => variable * 2외부 스코프 variable가 람다의 내부 범위에 실제로 정의되지 않은 람다 식을 사용하기 때문에이 동작이 트리거 됩니다.

C # 2의 익명 메소드뿐만 아니라 C # 3 +의 Lambda 표현식은 여전히 ​​실제 메소드를 작성합니다. 이러한 메소드에 변수를 전달하는 것은 약간의 딜레마를 포함합니다 (값으로 전달? 참조로 전달? 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;
}

어쨌든 “Closures”는 루프와 관련된 개념이 아니라 로컬 범위 변수의 익명 메소드 / 람다 식 사용 과 관련이 있습니다. 루프를 잘못 사용하면 클로저 트랩이 나타납니다.


답변

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