Post

C# 캡처 변수

캡처 변수

C# 캡처 변수

Git Source


  • 람다식 등을 구현할 때 로컬변수를 사용하는 경우, 이를 클로저(Closure)라 부르는데 로컬변수를 사용하는 경우 캡처 변수라는 것을 주의해야한다. ⚠

  • 다음 코드를 살펴보면 개발자가 기대한 값이 0, 1, 2, 3, 4 이지만 실제 실행하여 결과를 확인하면 5, 5, 5, 5, 5가 출력되는 것을 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void Main(string[] args)
{
    var actions = new List<Action>();

    for (int idx = 0; idx < 5; idx++)
    { 
        actions.Add(() => Console.WriteLine(idx));
    }

    foreach (var action in actions)
    {
        action.Invoke();
    }
}
  • 이를 해결해주기 위해서는 for문 scope안에 새로운 로컬변수를 할당하여 대리자에 넘겨주어야 한다. 이를 반영하여 수정한 코드는 다음과 같을 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void Main(string[] args)
{
    var actions = new List<Action>();

    for (int idx = 0; idx < 5; idx++)
    { 
        int temp = idx;
        actions.Add(() => Console.WriteLine(temp));
    }

    foreach (var action in actions)
    {
        action.Invoke();
    }
}
  • 위 코드를 실행하면 정상적으로 기대한 값이 출력되는 것을 확인할 수 있다.

  • 🤔 얼핏 보기엔 로컬 변수 idx가 값 복사가 이루어져 대리자에 넘어가는 것이 아닌가 싶지만, 클로저를 사용하게되면 동작방식이 다르다.

  • Reference에 있는 article을 살펴보면, 람다식에 사용되는 변수는 컴파일러로 인해 따로 익명의 private 클래스의 멤버 변수로 옮겨지고 람다식의 내용도 그 클래스의 메서드로 옮겨지게 된다.

  • 이에 따라 처음에 작성했던 코드는 컴파일러에 의해 다음과 같이 변경이 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class ComplierClass
{
    public int _i;
 
    public void _method()
    {
        Console.WriteLine(_i);
    }
}
 
static void Main(string[] args)
{  
    var actions = new List<Action>();
 
    ComplierClass complier = new ComplierClass();
 
    for (int complier._i = 0; complier._i < 5; complier._i++)
    {  
        actions.Add(complier._method);
    }
 
    foreach(var act in actions)
    {
        act.Invoke();
    }
}
  • for문 scope 밖에서 임시객체가 생성이되고, for문 안에서 임시객체의 _i 맴버에 계속 대입을 해주니 기대했던 값과 다르게 나온 것이다.

  • 그럼 수정된 코드는 컴파일러에 의해 어떻게 변경이 되었는지 다음 코드를 살펴보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class ComplierClass
{
    public int _temp;
 
    public void _method()
    {
        Console.WriteLine(_temp);
    }
}
 
static void Main(string[] args)
{  
    var actions = new List<Action>(); 
 
    for (int idx = 0; idx < 5; idx++)
    {  
        ComplierClass complier = new ComplierClass();
        complier._temp = i;
        actions.Add(complier._method);
    }
 
    foreach(var act in actions)
    {
        act.Invoke();
    }
}
  • 위 코드는 임시객체가 for문 scope안에서 생기는걸 확인할 수 있다. loop마다 새로이 객체가 생성되고 새로운 객체의 멤버에 대입을 한번만 해주니 기대했던 결과를 얻어낼 수 있는 것이다.

Reference

Codeproject Inside C# 2.0 Anonymous Methods

.NET Framework: 523. C# 람다(Lambda)에서 변수 캡쳐 방식

This post is licensed under CC BY 4.0 by the author.