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