54. 람다 클로저의 변수 캡처와 루프 변수 함정
난이도 중내 리뷰 · C#
해설 · C#
해설 — 람다 클로저의 변수 캡처와 루프 변수 함정
난이도: 중상
요약
C# 람다는 캡처한 변수의 값 스냅샷이 아니라 변수(저장 위치) 자체를 캡처한다. 컴파일러는
캡처된 지역을 **디스플레이 클래스(closure class)**의 필드로 승격(호이스팅)하고, 람다를 그
클래스의 메서드로 만든다. 여러 람다가 같은 변수를 캡처하면 같은 필드를 공유한다. 그래서
for 루프처럼 반복 변수 하나를 계속 갱신하면 모든 람다가 그 하나의 최종값을 본다. 반면
foreach는 C# 5부터 반복마다 새 변수를 도입하므로 각 람다가 자기 값을 캡처한다.
예측 출력
3 3 3
10 20 30
99
- (1) for:
i는 루프 전체에서 하나의 변수. 세 람다가 같은i를 공유하고, 실행 시점엔 루프가 끝나i == 3→3 3 3. - (2) foreach: C# 5+에서 반복 변수
n은 반복마다 새로 생성되어 각 람다가 별도 변수를 캡처 →10 20 30. (C# 4 이하였다면for처럼30 30 30이 됐다.) - (3):
show는 변수x를 캡처. 호출 시점의x값을 읽으므로x=99반영 →99.
핵심 개념
- 캡처 대상은 변수: 클로저는 변수의 현재 값을 복사하지 않고, 그 변수의 저장소를 공유한다. 캡처 후 원본을 바꾸면 람다도 새 값을 본다((3)).
- 디스플레이 클래스: 캡처된 지역들은 컴파일러가 생성한 클래스의 인스턴스 필드가 되고, 스택이 아니라 힙에 산다(클로저가 살아있는 한 유지). 캡처는 힙 할당·수명 연장을 유발.
- for vs foreach:
for의 반복 변수는 루프 스코프의 단일 변수.foreach는 C# 5부터 각 반복이 새 변수 → 캡처 시맨틱이 다르다.
단계별 내부 동작
- (1) 컴파일러는
i를 디스플레이 클래스 필드로 승격. 세 람다는 같은 인스턴스의 같은 필드를 참조. 루프가 필드를 0→1→2→3으로 갱신하고 종료. 나중 실행 시 모두 3. - (2) 각 반복마다 새 디스플레이 클래스 인스턴스(또는 새 변수 슬롯)가 만들어져
n의 그 반복 값을 담는다. 람다마다 다른 인스턴스를 참조. - (3)
x필드를 캡처.x=99가 같은 필드를 갱신하므로 호출 시 99.
흔한 오해·함정
- "람다는 그 시점의 값을 저장한다"(X) — 변수를 캡처한다. for 루프에서 흔한 버그.
- for 루프 캡처 교정: 반복 안에서 지역 복사를 만들어 그것을 캡처한다.
for (int i = 0; i < 3; i++) { int copy = i; a1.Add(() => Console.Write(copy + " ")); } // -> 0 1 2 - 캡처는 의도치 않은 수명 연장/힙 할당을 만든다 — 큰 객체나
this를 무심코 캡처하면 GC 압박·누수. 게임서버 핫패스에서 매 프레임 람다 캡처는 할당을 유발하니 주의(정적 람다/캐싱). foreach도 C# 4 이하 타깃이면for와 같은 함정 — 대상 언어 버전 확인.
면접 포인트
- "값이 아니라 변수를 캡처"의 의미와 for 루프 캡처 버그, 지역 복사 교정.
- C# 5의 foreach 반복 변수 변경과 그 이유.
- 클로저의 힙 할당·수명 연장(디스플레이 클래스)과 성능 함의.