Programming/C# & Unity
C# Foreach Closure
장형이
2023. 6. 18. 15:25
C#에서 람다를 사용하여 람다 밖에서 사용 중인 변수를 가져오면(캡처하면) 항상 레퍼런스로 전달되는 것 처럼 처리가 된다. (람다-클로저 관련 원리에 대한 간략한 포스팅)
for (int i = 0; i < 10; ++i)
{
new Thread(() =>
{
Thread.Sleep(1000);
Console.WriteLine(i);
}).Start();
}
// 10이 10번 출력됨.
위의 경우에는 하나의 i가 10번 레퍼런스 전달되었고, 1초뒤에 forloop가 전부 끝나서 10이 된 i가 10번 출력되게 된다.
그렇다면 foreach를 사용하면 어떨까?
foreach (int i in Enumerable.Range(1, 10))
{
new Thread(() =>
{
Thread.Sleep(1000);
Console.WriteLine(i);
}).Start();
}
// 1~10 까지 잘 출력됨.
foreach는 놀랍게도 원하는 대로 출력된다.
반복자인 int i가 하나의 원본이 아닌 것 처럼 보이는데 어떻게 된 일일까?
// C# 5.0 이전 -------------------------------------------
var it = Enumerable.Range(1, 10).GetEnumerator();
int i;
while (it.MoveNext())
{
i = it.Current; // 하나의 주소가 계속 캡쳐되어 값이 불안정함.
new Thread(() =>
{
Thread.Sleep(1000);
Console.WriteLine(i);
}).Start();
}
// 10이 10번 출력됨.
// C# 5.0 이후 -------------------------------------------
var it = Enumerable.Range(1, 10).GetEnumerator();
while (it.MoveNext())
{
int i = it.Current; // 항상 새로운 값으로 캡쳐되어 반복자가 닫힘.
new Thread(() =>
{
Thread.Sleep(1000);
Console.WriteLine(i);
}).Start();
}
// 1~10 까지 잘 출력됨.
알고 보니 C# 5.0부터 foreach 문은 이런 문제가 발생하지 않도록 하기 위해 위와 같이 항상 반복자를 새로 캡처하여 넘기는 것으로 변경되었다. 이른바 [루프 변수 닫기]라고 한다.
덕분에 Async 로직이나 지연 평가를 하는 부분에서 복잡한 못한 문제가 발생하지 않고 있었다. 갓샾.
관련 자료.
https://learn.microsoft.com/ko-kr/archive/blogs/ericlippert/closing-over-the-loop-variable-part-two