← 문제로

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(); } 를 더한 것이다.

단계별 내부 동작

  1. Numbers() → 상태 머신 객체 반환(본문 미실행). "created".
  2. 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 반환 → 종료.
  3. 둘째 foreach: 같은 seq 라도 GetEnumerator() 를 다시 호출해 새 상태 머신을 얻으므로 처음부터 다시 실행 → "start""end" 가 한 번 더. (IEnumerable 은 재열거 가능; 매 열거가 본문을 재실행)
  4. 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열거 시점에 실행된다. 그래서 쿼리 변수를 만든 뒤 소스가 바뀌면 결과도 바뀐다.

면접 포인트

  1. yield 메서드는 컴파일러가 만든 상태 머신이며 호출은 지연 실행 — "created""start" 보다 먼저.
  2. IEnumerable 재열거는 매번 GetEnumerator()재실행 — 비싼 소스는 ToList() 로 구체화.
  3. 예외·인자 검증도 지연된다 → 검증은 비-이터레이터 래퍼에서. LINQ 지연 실행과 동일 원리.