이터레이터 패턴
개요
재생 목록을 순서대로 읽는 코드가 내부 배열의 인덱스를 직접 다루면, 저장 방식을 바꿀 때 호출부도 고쳐야 한다. 무엇을 담는지와 어디까지 읽었는지를 나누면 순회 방법을 감출 수 있다.
이터레이터(Iterator), 즉 반복자는 다음 항목으로 이동하고 현재 항목을 읽는 역할이다. 목록은 항목을 보관하고, 각 반복자는 자기 순회 위치를 가진다.
UML
Playlist가 반복자를 생성한다. 호출부는 IEnumerator<string>의 MoveNext()와 Current를 사용하며, 예제의 yield return을 C# 컴파일러가 반복자 구현으로 바꾼다.
역사
GoF는 1994년 Design Patterns에서 Iterator를 행위 패턴으로 정리했다. 집합체의 내부 표현을 공개하지 않고 요소에 차례로 접근하는 구조다.1
왜 사용하는가?
호출부는 내부 배열을 받지 않고 순서대로 항목을 읽는다. 같은 목록을 여러 곳에서 읽어도 반복자마다 위치를 따로 둘 수 있다.
지연 조회에서는 순회 중에도 I/O나 실패가 일어날 수 있다. 이 예제는 생성 시 복사한 문자열 배열만 읽으며, 변경 중인 목록이나 원격 스트림의 동작을 보장하지 않는다.
예제
Playlist를 foreach로 순회한다. 내부 배열은 호출부에 노출하지 않는다.
using System;
using System.Collections.Generic;
using System.Linq;
var playlist = new Playlist(new[] { "intro", "outro" });
foreach (string track in playlist) Console.WriteLine(track);
sealed class Playlist : IEnumerable<string>
{
private readonly string[] tracks;
public Playlist(IEnumerable<string> tracks) => this.tracks = tracks.ToArray();
public IEnumerator<string> GetEnumerator()
{
foreach (string track in tracks) yield return track;
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}
intro
outroforeach는 반복자 프로토콜을 사용한다. yield return은 항목을 하나씩 제공할 구현을 간단히 작성하는 C# 문법이다.
범위 넓힌 예제
같은 목록에서 반복자를 두 개 만들고 서로 다른 속도로 이동한다.
using System;
using System.Collections.Generic;
using System.Linq;
var playlist = new Playlist(new[] { "intro", "outro" });
using var first = playlist.GetEnumerator();
using var second = playlist.GetEnumerator();
if (first.MoveNext()) Console.WriteLine("first: " + first.Current);
if (second.MoveNext()) Console.WriteLine("second: " + second.Current);
if (first.MoveNext()) Console.WriteLine("first: " + first.Current);
Console.WriteLine("first has next: " + first.MoveNext());
Console.WriteLine("second: " + second.Current);
sealed class Playlist : IEnumerable<string>
{
private readonly string[] tracks;
public Playlist(IEnumerable<string> tracks) => this.tracks = tracks.ToArray();
public IEnumerator<string> GetEnumerator()
{
foreach (string track in tracks) yield return track;
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}
first: intro
second: intro
first: outro
first has next: False
second: intro첫 반복자를 한 칸 더 움직여도 두 번째 반복자의 Current는 바뀌지 않는다. MoveNext()가 false를 반환한 뒤에는 Current를 읽지 않는다.
같이 보기
각 예제는 독립된 .NET 10 콘솔 프로그램이다.