C# 람다 클로저 원리
개요
C#의 람다식의 사용법과 어떻게 작동하는지 간략하게 정리한다.
람다
int number = 5;
// input에 5를 더하여 리턴하는 익명 함수를 담은 변수 action.
var action = (int input) => { return number + input; };
// 5 + 10이 리턴되어 15가 출력된다.
Console.WriteLine(action(10)); // 출력: 15
위 코드에서 "(int input) => { return number + input; };" 구문을 람다 식이라고 부르며, 함수지만 이름이 없어서 무명 함수(메서드)라고도 부른다.
이때, 익명 함수 외부에서 가져온 변수 number를 캡처된 변수라고 부른다.
그리고 캡처가 완료되어 사용가능한 상태가 된 익명 함수를 클로져라고 부르고, 위 예제에서는 그 클로져를 action이라는 변수가 담고 있다.
캡처는 레퍼런스
int number = 5;
// number라는 캡쳐된 변수에 input을 집어 넣는 무명함수.
// action에 캡쳐가 폐쇄된(closure) 무명함수가 저장되었다.
var action = (int input) => { number = input; };
// action에 인자를 10으로 실행한다.
action(10);
// 실제로 number가 수정되었다.
Console.WriteLine(number); // 출력: 10
캡쳐된 변수는 레퍼런스로 넘어가서 무명 함수 내에서 수정하면 실제로 캡처된 변수의 값이 변경된다.
컴파일러가 생성해내는 람다 코드
public class Program {
public void Main() {
int number = 5;
Console.WriteLine(number);
var action = (int input) => { return number + input; };
Console.WriteLine(action(10));
}
}
위 코드를 실제로 컴파일러는 아래의 코드로 생성해 낸다.
[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
public int number;
internal int <Main>b__0(int input)
{
return number + input;
}
}
public void Main()
{
<>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
<>c__DisplayClass0_.number = 5;
Console.WriteLine(<>c__DisplayClass0_.number);
Func<int, int> func = new Func<int, int>(<>c__DisplayClass0_.<Main>b__0);
Console.WriteLine(func(10));
}
1. 먼저 클래스를 만드는 코드를 추가한다. (<>c__DisplayClass2_0 클래스)
2. 익명 함수를 (1.)에서 만든 클래스 안으로 집어넣는다.
3. 캡처되어야 할 변수를 클래스 멤버 변수로 만든다.
4. 람다를 사용하는 코드에서 (1.)에서 만든 클래스의 객체를 생성하는 코드로 바꾼다.
5. 객체를 생성한 다음에 캡쳐되어야 할 변수를 클래스의 객체 멤버 변수에 넣는다.
6. 캡처된 변수를 사용하는 부분을 모두 (5.)의 객체 멤버 변수를 참조하도록 수정한다.
위처럼 코드가 생성되기 때문에 클로져가 제대로 작동하며, 레퍼런스 타입과 primitive 타입들 모두 캡쳐 변수라면 레퍼런스로 넘어가는 것처럼 처리가 되게 된다.
참고
람다식은 인라인 함수로 생성되며 인라인 최적화 될 수 있다.
C++도 거의 똑같은 원리로 생성해내니 참고하자.
https://sharplab.io/ 사이트에서 컴파일러 생성 코드를 쉽게 확인 할 수 있다.