2. 클로저 캡처와 지연 실행(for vs foreach, LINQ)
난이도 중내 리뷰 · C#
해설 · C#
해설 — 클로저 캡처와 지연 실행(for vs foreach, LINQ)
난이도: 중
답변 프레임워크: 요약 → 핵심 개념 → 단계별 내부 동작 → 흔한 오해·함정 → 면접 포인트
요약
예상 출력:
3 3 3
10 20 30
1,2,3,4
- for 블록 →
3 3 3:for (int i...)의i는 루프 전체에서 하나의 변수. 세 람다가 모두 같은i를 캡처하고, 실행 시점엔 루프가 끝나i == 3. - foreach 블록 →
10 20 30: C# 5 부터 foreach 반복 변수n은 반복마다 새 변수. 각 람다가 서로 다른n을 캡처한다. - LINQ 블록 →
1,2,3,4:Where는 지연 실행. 람다x > threshold는 변수threshold를 캡처하고, 실제 열거(string.Join)는threshold = 0이 된 뒤라 모두 통과.
핵심 개념
- 클로저는 "변수"를 캡처한다(값 복사 아님): 컴파일러는 캡처된 지역변수를 숨은 클로저 클래스의 필드로 끌어올린다(heap). 람다는 그 필드를 참조하므로, 변수가 나중에 바뀌면 람다도 바뀐 값을 본다.
- 루프 변수의 수명:
for의 제어변수는 루프 한 번만 선언 → 모든 반복이 같은 변수(같은 클로저 필드)를 공유.foreach의 반복 변수는 C# 5 이후 각 반복마다 새 변수로 취급 → 반복별로 별도 클로저 필드. (C# 5 이전엔 for 처럼 공유되어 같은 함정이 있었다.)
- 지연 실행(deferred execution):
Where/Select등은 호출 즉시 계산하지 않고 이터레이터를 반환하고,foreach/ToList/Join등으로 열거할 때 평가된다. 그래서 "정의 후 캡처 변수 변경"이 결과에 반영된다.
단계별 내부 동작
- for 블록: 컴파일러가
i를 클로저 객체의 필드로 승격. 세 람다가 동일 객체의i참조.foreach (var a in forActions) a();실행 시i==3→3 3 3. - foreach 블록: 각 반복에서 새
n(반복별 클로저). 람다들이 10/20/30 을 각자 캡처 →10 20 30. - LINQ 블록:
q = src.Where(...)는 평가 지연.threshold = 0후string.Join이 열거 →x > 0으로 1,2,3,4 모두 통과 →1,2,3,4.
흔한 오해·함정
- "람다가 그때의 값을 찍는다"는 오해: 값이 아니라 변수를 캡처한다. 루프에서 즉시 값을
고정하려면 반복마다 지역 복사:
for (...) { int copy = i; actions.Add(() => ...copy...); }. - for 루프 + Task/이벤트 핸들러: 게임서버에서
for로 콜백/타이머를 등록하면 모두 마지막 값을 본다(흔한 버그). 복사본 캡처 또는 foreach 사용. - 지연 실행의 함정: 쿼리를 여러 번 열거하면 매번 재평가(부수효과/성능). 소스가 바뀌면
결과도 바뀐다. 고정하려면
ToList()/ToArray()로 즉시 구체화. - 캡처로 인한 수명 연장: 람다가 큰 객체/
this를 캡처하면 그 수명이 람다만큼 길어진다 (이벤트 핸들러 누수의 원인). 필요한 값만 지역으로 캡처.
면접 포인트
- for vs foreach 캡처 차이와 그 이유(루프 변수의 수명/스코프, C# 5 변경). 즉시 고정하는 법.
- 지연 실행이 무엇이고 언제 평가되는가,
ToList()로 구체화하는 이유(재평가/부수효과). - 클로저가 변수를 heap 필드로 승격 →
this/큰 객체 캡처가 GC 압박·수명 연장·핸들러 누수를 부르는 메커니즘.