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-considered-harmful

 

Closing over the loop variable considered harmful

Table of contents Closing over the loop variable considered harmful Article 11/12/2009 In this article --> (This is part one of a two-part series on the loop-variable-closure problem. Part two is here.) UPDATE: We are taking the breaking change. In C# 5, t

learn.microsoft.com

https://learn.microsoft.com/ko-kr/archive/blogs/ericlippert/closing-over-the-loop-variable-part-two