로컬 변수를 참조하는 무명메서드 또는 람다식
무명메서드나 람다식을 Closure라고 생각하면 안된다. 특정한 조건에서 Closure를 구현할 수 있다. 여기서 특정한 조건은 함수 바깥에 있는 로컬 변수를 참조할 때를 얘기한다. 아래 무명메서드의 Closure를 보면 이해가 될 것이다.
public void Foo()
{
int i = 1; // 로컬 변수
Action<string> action = delegate (string input)
{
Console.WriteLine($"{i} {input}");
}
action("Hello");
// 1 Hello
}
Closure는 로컬 변수를 참조하고 있으므로 로컬 변수가 변하면 Closure 내부도 변한다. 이를 모르고 무명메서드와 람다식을 사용한다면 기댓값과 다른 결과를 맞이할 수 도 있다.
public void Foo()
{
int i = 1;
Action<string> action = delegate (string input)
{
Console.WriteLine($"{i} {input}");
}
i = 2;
action("Hello");
// 기대하는 값이 1 Hello일 수도 있지만 그렇지 않다.
// 출력 값: 2 Hello
}
아래의 결과는 어떻게 될까?
class Program
{
public static List<int> Values = new List<int>() { 1, 6, 5, 3, 2 };
static void Main(string[] args)
{
List<Action> sumAction = new List<Action>();
for (int idx = 0; idx < Values.Count; idx++)
{
sumAction.Add(() => Console.WriteLine(idx + Values[idx]));
}
// Do Something
foreach (var sum in sumAction)
{
sum();
}
Console.ReadLine();
}
}
for문은 로컬 변수 idx를 Values.Count만큼 증가시킨 후 Values.Count와 비교하고 루프 문을 종료하게 될 텐데. 여기서 sumAction을 호출하게 된다면 idx가 배열 인덱스를 벗어난 상태이기 때문에 System.ArgumentOutOfRangeException를 보게 될 것이다.
Closure는 컴파일러에 의해 구현된다. 컴파일러는 Nested Class를 활용해 메서드에 변형을 주는데 자세한 내용은 아래를 참고하고 이번 섹션에서는 생략한다.