← 문제로

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 등으로 열거할 때 평가된다. 그래서 "정의 후 캡처 변수 변경"이 결과에 반영된다.

단계별 내부 동작

  1. for 블록: 컴파일러가 i 를 클로저 객체의 필드로 승격. 세 람다가 동일 객체의 i 참조. foreach (var a in forActions) a(); 실행 시 i==33 3 3.
  2. foreach 블록: 각 반복에서 새 n(반복별 클로저). 람다들이 10/20/30 을 각자 캡처 → 10 20 30.
  3. LINQ 블록: q = src.Where(...) 는 평가 지연. threshold = 0string.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 를 캡처하면 그 수명이 람다만큼 길어진다 (이벤트 핸들러 누수의 원인). 필요한 값만 지역으로 캡처.

면접 포인트

  1. for vs foreach 캡처 차이와 그 이유(루프 변수의 수명/스코프, C# 5 변경). 즉시 고정하는 법.
  2. 지연 실행이 무엇이고 언제 평가되는가, ToList() 로 구체화하는 이유(재평가/부수효과).
  3. 클로저가 변수를 heap 필드로 승격 → this/큰 객체 캡처가 GC 압박·수명 연장·핸들러 누수를 부르는 메커니즘.