← 문제로

50. 정적 지역 함수(static local function) vs 람다

난이도 중
내 리뷰 · C#
해설 · C#

해설 — 정적 지역 함수(static local function) vs 람다

난이도: 중상

요약

람다가 지역 변수를 캡처하면 컴파일러가 클로저 클래스(힙 객체)를 만들어 그 변수를 필드로 끌어올린다(hoisting). 그래서 캡처한 변수는 참조로 공유되어 정의 이후의 변경도 보인다. 정적 지역 함수는 캡처가 금지되어(=클로저 할당 없음) 일반 정적 메서드 처럼 컴파일되고, 캡처 없는 람다는 한 번 만들어 정적 필드에 캐시된다.

실행 결과(예측)

lambda factor=20
Mul=12
square=25
0 1 2 

핵심 개념 / 단계별

  • (a) 캡처 람다 → 클로저 할당: factor 를 캡처하는 람다는 컴파일러가 생성한 디스플레이 클래스(예: <>c__DisplayClass)의 인스턴스에 factor 를 필드로 저장한다. lambda 델리게이트는 그 인스턴스를 가리킨다. 따라서 factor = 20 은 같은 필드를 바꾸고, 호출 시 20 이 출력된다(값 복사가 아니라 참조 캡처).
  • (b) 정적 지역 함수 → 할당 없음: static int Mul(...) 은 바깥 변수를 캡처할 수 없으므로(‘static’이 이를 컴파일 타임에 강제) 클로저가 필요 없다. 일반 (정적) 메서드로 낮춰지고, 직접 호출은 델리게이트 할당도 없다. 인라인·최적화에 유리.
  • (c) 캡처 없는 람다 → 1회 캐시: x => x*x 는 아무것도 캡처하지 않아 컴파일러가 정적 필드에 델리게이트 인스턴스를 한 번만 만들어 캐시한다(호출마다 재할당 없음).
  • (d) 반복문 캡처: 반복마다 새 지역 변수 copy 를 캡처하므로 각 클로저가 자신만의 copy 를 갖는다 → 0 1 2. 만약 루프 변수 i 를 직접 캡처했다면(그리고 하나의 변수를 공유했다면) 흔한 함정처럼 3 3 3 이 될 수 있다. C# 5+ 의 foreach 변수는 반복마다 새 변수라 안전하지만, for 의 i 는 하나의 변수라 여기서는 일부러 copy 를 도입했다.

흔한 오해·함정

  • "람다는 항상 힙 할당" — 아니다. 캡처가 없으면 캐시되고, 캡처가 있으면 할당된다. 캡처 대상이 this(인스턴스 멤버 접근)여도 클로저(또는 this 참조)가 생긴다.
  • 정적 지역 함수 + Span/인자 전달로 핫패스의 클로저 할당을 제거하는 것이 흔한 최적화. 필요한 값은 캡처 대신 매개변수로 넘긴다.
  • 부지불식 캡처: 람다 안에서 인스턴스 필드/메서드를 쓰면 this 를 캡처해 예상치 못한 수명 연장(메모리 누수)·할당이 생긴다. static 람다(C# 9+)로 캡처를 금지해 방지 가능.
  • LINQ 체인의 람다가 반복적으로 캡처를 할당하면 tight loop 에서 GC 압력이 된다.

면접 포인트

  • 캡처 유무가 할당을 좌우한다: 캡처 없음=캐시, 캡처=클로저 인스턴스, static 지역함수=할당 없음.
  • 캡처는 값 복사가 아니라 변수 hoisting(참조 공유) — 정의 후 변경이 보인다.
  • 핫패스에서 static 지역 함수/static 람다 + 매개변수 전달로 GC 압력 제거.