6. yield return 이터레이터의 상태 머신과 지연 실행
난이도 중내 리뷰 · C#
해설 · C#
해설 — yield return 이터레이터의 상태 머신과 지연 실행
난이도: 중
요약
yield return 이 있는 메서드를 호출하면 본문이 즉시 실행되지 않는다. 컴파일러는 그 메서드를 IEnumerable<T>/IEnumerator<T> 를 구현하는 상태 머신 클래스로 바꾸고, 호출은 그 객체만 만들어 돌려준다(지연 실행). 실제 코드는 foreach(내부적으로 GetEnumerator() + 반복 MoveNext())가 돌 때, yield return 마다 멈췄다 재개하며 조금씩 실행된다.
출력 예측
created
start
yield 0
got 0
yield 1
got 1
yield 2
got 2
end
--- second pass ---
start
yield 0
got 0
yield 1
got 1
yield 2
got 2
end
--- throwing iterator ---
called WithThrow
핵심 개념
- 지연 실행(deferred execution):
Numbers()호출은 상태 머신 객체 생성만 한다. 그래서"created"가"start"보다 먼저 나온다. 본문은 첫MoveNext()부터 실행. - 상태 머신: 컴파일러가
MoveNext()/Current를 가진 클래스를 만든다. 지역 변수i는 그 클래스의 필드가 되어 호출 사이에 상태가 보존된다.MoveNext()는 "다음yield return까지 실행하고 멈춤",Current는 마지막으로 내보낸 값. - foreach 의 정체:
foreach (x in seq)는var e = seq.GetEnumerator(); while (e.MoveNext()) { x = e.Current; ... }에try/finally { e.Dispose(); }를 더한 것이다.
단계별 내부 동작
Numbers()→ 상태 머신 객체 반환(본문 미실행)."created".- 첫
foreach:GetEnumerator()로 enumerator 확보, 첫MoveNext()→ 본문 시작"start"→ 루프"yield 0"후yield return 0에서 멈춤.Current==0→"got 0". 이후MoveNext()마다 재개해"yield 1"/got 1,"yield 2"/got 2, 마지막MoveNext()가 루프를 빠져나와"end"출력 후false반환 → 종료. - 둘째
foreach: 같은seq라도GetEnumerator()를 다시 호출해 새 상태 머신을 얻으므로 처음부터 다시 실행 →"start"…"end"가 한 번 더. (IEnumerable은 재열거 가능; 매 열거가 본문을 재실행) WithThrow()는 호출만 했으므로 본문 미실행 →"before first yield"도, 예외도 나지 않는다."called WithThrow"만 출력. 만약bad를 열거하면 첫MoveNext()에서"before first yield"후 1 을 내보내고, 그다음MoveNext()에서throw가 관측된다(예외도 지연된다).
흔한 오해·함정
- "메서드를 호출하면 본문이 실행된다" → 이터레이터/
IEnumerable반환은 열거 전까지 실행 안 됨. 인자 검증을 이터레이터 본문에 두면 호출 시점이 아니라 열거 시점에 터진다(그래서 인자 검증은 별도 비-이터레이터 래퍼 메서드에 둔다). - 다중 열거(multiple enumeration):
IEnumerable을 여러 번 돌리면 매번 재실행된다. DB 쿼리/네트워크 같은 비싼 소스면 매 열거마다 다시 호출돼 성능·일관성 문제. 필요하면.ToList()로 구체화(materialize). - LINQ 의
Where/Select등도 같은 지연 실행이다(이터레이터로 구현). 쿼리 정의 시점이 아니라foreach/ToList/Count등 열거 시점에 실행된다. 그래서 쿼리 변수를 만든 뒤 소스가 바뀌면 결과도 바뀐다.
면접 포인트
yield메서드는 컴파일러가 만든 상태 머신이며 호출은 지연 실행 —"created"가"start"보다 먼저.IEnumerable재열거는 매번GetEnumerator()로 재실행 — 비싼 소스는ToList()로 구체화.- 예외·인자 검증도 지연된다 → 검증은 비-이터레이터 래퍼에서. LINQ 지연 실행과 동일 원리.