람다함수를 delegate에 등록해줄 때,
호출부의 지역변수를 사용하고 싶을때가 있다.
이를 변수를 Capture한다고 하는데
무슨 얘기냐 하면 다음 코드를 보자
class Program
{
static void Main(string[] args)
{
Action countZeroToFour = null;
for(int i=0; i<5; i++)
{
countZeroToFour += () => Console.WriteLine(i);
}
countZeroToFour.Invoke();
}
}
Action을 Invoke 했을 때, 0~4까지의 숫자를 차례대로 출력하게 하고 싶어서 위와 같이 작성했다.
실행 결과는?
5
5
5
5
5
전혀 다른 결과가 나온다.
프로젝트 진행중에 각 오브젝트에 이벤트를 할당해주면서 저런식으로 작성했었는데
계속 이상한 값이 들어가서 엄청 헤맸었다. ㅠㅠ
그래서 열심히 구글링해본 결과 답을 얻을 수 있었다.
왜 이런 결과가 나올까?
우선 컴파일러가 람다함수를 어떻게 바꿔주는지에 대한 이해가 필요하다.
https://www.codeproject.com/Articles/15624/Inside-C-2-0-Anonymous-Methods#4
Inside C# 2.0 Anonymous Methods
Understand the internal working of anonymous methods in C# 2.0.
www.codeproject.com
해당 글을 읽어보면, 컴파일러는
private inner class를 만들고
람다함수를 private 함수로 등록한다고 되어있다.
그래서 위의 코드는 컴파일시 다음과 같이 컴파일러에 의해 바뀌게 된다.
class Program
{
private class InnerClass
{
public int i;
private void InstanceMethod()
{
Console.WriteLine(i);
}
}
static void Main(string[] args)
{
Action countZeroToFour = null;
InnerClass localObject = new InnerClass();
for(int i=0; i<5; i++)
{
localObject.i = i;
countZeroToFour += () => localObject.InstanceMethod;
}
countZeroToFour.Invoke();
}
}
코드를 잘 살펴보면 InnerClass 인스턴스는 하나만 만들어지는데
for-loop에서 인스턴스의 i에 계속 할당을 해주는걸 볼 수 있다.
심지어 결과가 4가 아닌 5로 나오는걸로 봐서
for문이 break될때도 할당이 일어남을 알 수 있다.
따라서 5가 5번 나오는 결과는 이해가 간다.
여기서 드는 궁금증은 InnerClass의 인스턴스인 localObject 선언 위치의 기준은 무엇이고,
localObject.i에 대한 연산을 하는 위치의 기준은 어떻게 되는지이다.
컴파일러의 입장으로 생각해보자.
흐름대로 잘 가다가 람다함수를 만나는 순간, 헉 하고 당황하여
우선은 람다함수에서 지역변수(람다 기준에서의 외부 변수)에 접근하는지 먼저 살펴볼것이다.
우리의 코드는 for-loop scope의 지역변수인 i를 람다함수에서 끌어다 쓰고 있으므로,
사용하는 지역변수의 scope에서 localObject.i에 대한 연산을 해준다.
인스턴스 생성은 그럼 사용하는 지역변수를 모두 포함하는 scope에서 해줄 수 밖에 없다는게 이해되기 시작한다.
따라서 for문 밖에서 인스턴스 생성이 일어나게 된다.
그럼 원래 우리가 의도한대로 동작이 되게 하려면 어떻게 해야할까?
class Program
{
static void Main(string[] args)
{
Action countZeroToFour = null;
for(int i=0; i<5; i++)
{
int temp = i; // for-loop scope보다 더 좁은 scope의 지역변수 선언
countZeroToFour += () => Console.WriteLine(temp); // i가 아닌 temp를 붙여준다
}
countZeroToFour.Invoke();
}
}
0
1
2
3
4
추가된건 temp라는 지역변수 하나뿐이다.
그럼 컴파일러는 이를 어떻게 해석할까?
temp는 for-loop보다 더 좁은 범위의 scope이어서
다음과 같이 코드가 바뀌게 된다.
class Program
{
private class InnerClass
{
public int i;
private void InstanceMethod()
{
Console.WriteLine(i);
}
}
static void Main(string[] args)
{
Action countZeroToFour = null;
for(int i=0; i<5; i++)
{
InnerClass localObject = new InnerClass();
localObject.i = i;
countZeroToFour += () => localObject.InstanceMethod;
}
countZeroToFour.Invoke();
}
}
이제 결과가 제대로 나오는게 이해가 된다.
정리를 하자면,
InnerClass의 생성 위치에 따라 지역변수 capture 결과는 천차 만별이다.
이 위치는 람다함수에서 사용할 지역변수의 scope를 보면 된다.
//로 scope를 표현해보자면 다음과 같다.
class Program
{
static void Main(string[] args)
{
Action countZeroToFour = null;
//for(int i=0; i<5; i++)
//{
// countZeroToFour += () => Console.WriteLine(i);
//}
countZeroToFour.Invoke();
}
}
class Program
{
static void Main(string[] args)
{
Action countZeroToFour = null;
for(int i=0; i<5; i++)
{
//int temp = i;
//countZeroToFour += () => Console.WriteLine(temp);
}
countZeroToFour.Invoke();
}
}
참고로 foreach는 조금 특별한 scope를 가지고 있어서 지역변수 capture side-effect가 발생하지 않고
제대로 동작한다.
MSDN에서 람다함수의 변수 scope를 다음과 같이 정하고 있다.
Lambda expressions - C# reference
Learn about C# lambda expressions that are used to create anonymous functions.
docs.microsoft.com
The following rules apply to variable scope in lambda expressions:
- A variable that is captured will not be garbage-collected until the delegate that references it becomes eligible for garbage collection.
- Variables introduced within a lambda expression are not visible in the enclosing method.
- A lambda expression cannot directly capture an in, ref, or out parameter from the enclosing method.
- A return statement in a lambda expression doesn't cause the enclosing method to return.
- A lambda expression cannot contain a goto, break, or continue statement if the target of that jump statement is outside the lambda expression block. It's also an error to have a jump statement outside the lambda expression block if the target is inside the block.
'etc > C#' 카테고리의 다른 글
[C#] switch 제어문 (0) | 2021.07.16 |
---|---|
[C#] 문자열 보간(Interpolation) (0) | 2021.07.15 |
[C#] Property(프로퍼티), C#만의 특별한 기능 (0) | 2021.07.13 |
[C#] Null-Safety를 지원하는 C# (0) | 2021.07.13 |
[C#] Lambda Expression(람다 식) 이란? (0) | 2021.07.12 |