저자: Jesse Liberty, 역 한동훈(traxacun @ unitel.co.kr)
원문기사: http://www.ondotnet.com/pub/a/dotnet/2004/06/07/liberty.html
컬렉션과 비슷한 클래스를 작성할 때는 foreach 문을 사용해서 컬렉션의 각 항목을 반복할 수 있게 작성하는 것이 좋다.
Programming C#, 3판에서 소개했던 예제의 간단한 컬렉션 예제를 통해서 C# 2.0에서 보다 쉬워진 Iterator 기능에 대해 살펴보자.
여기서는 간단한 ListBox 예제를 만들었다. ListBox의 인터페이스는 무시하고 단순히 문자열 컬렉션이라 생각하자.(전체 예제를
다운로드 할 수 있다)
설명을 위해 ListBox 클래스는 최대한 간단하게 하자. 다음 예제는 생성자에 문자열을 8개까지 담을 수 있는 배열을 만들고, 인자로 전달된 문자열을 foreach를 사용하여 각 배열에 문자열을 넣는다.
public ListBox(params string[] initialStrings)
{
// allocate space for the strings
strings = new String[8];
// copy the strings passed in to the constructor
foreach (string s in initialStrings)
{
strings[ctr++] = s;
}
}
ListBox에는 문자열을 추가하고 배열에 있는 문자열의 수를 반환하는 Add 메서드가 있다.
// 컬렉션에 문자열 추가(카운트를 증가시킨다)
public void Add(string theString)
{
strings[ctr] = theString; // 문자열을 추가한다
ctr++;
}
// 현재 문자열 배열의 수를 반환한다
public int GetNumEntries()
{
return ctr;
}
실제 응용 프로그램에서는 고정폭 배열 대신에 ArrayList 같은 것을 사용할 것이다. 따라서, 예제를 간단하게 하기 위해 배열의 경계 값에 대한 에러 검사를 하지 않는다.
보통의 컬렉션들이 foreach를 사용해서 루프를 돌 수 있는 것처럼 ListBox 컬렉션도 foreach 루프를 사용하여 모든 문자열을 출력할 수 있다면 편할 것이다. ListBox 컬렉션에 foreach 루프를 사용할 수 있다면 다음과 같이 코드를 작성할 수 있을 것이다.
ListBox lb = new ListBox();
foreach (string s in lb)
{
// 문자열 처리 코드 부분
}
foreach 루프를 사용하기 위해서는 IEnumerable 인터페이스를 구현해야 한다.
public class ListBox : IEnumerable
{
IEnumerable 인터페이스는 IEnumerator 인터페이스를 구현한 객체를 반환해야 하는 GetEnumerator 메서드만 가진다. IEnumerator를 구현하고 ListBox 객체를 나열하는 방법을 알고 있는 객체를 반환하기를 원할 것이다. 다음과 같이 ListBoxEnumerator를 생성한다.
public IEnumerator GetEnumerator()
{
return new ListBoxEnumerator();
}
이제 ListBox를 foreach 루프에 사용할 수 있다.
ListBox lbt =
new ListBox("Hello", "World");
// 문자열을 추가한다.
lbt.Add("Who");
lbt.Add("Is");
lbt.Add("John");
lbt.Add("Galt");
foreach (string s in lbt)
{
Console.WriteLine("Value: {0}", s);
}
문자열 Hello, World를 사용하여 인스턴스를 생성하고, 문자열을 4개 추가한 다음 foreach 루프에서 각 요소를 반복하며, 각 루프마다 문자열을 출력한다. 실행 결과는 다음과 같다.
Hello
World
Who
Is
John
Galt
IEnumerator 구현
이제 ListBoxEnumerator 객체는 Ienumerator 인터페이스를 구현해야 하며, ListBox 클래스를 어떻게 처리할 것인지 알고 있어야한다. 여기서는 ListBox 클래스의 문자열을 반복하면서 어떻게 배열로 접근하는지 알고 있어야한다.
IEnumerable 클래스와 IEnumerator 클래스의 관계는 다소 이해하기 어려울 수 있다. IEnumerator 클래스를 구현하는 가장 좋은 방법은 IEnumerable 클래스안에 중첩 클래스로 작성하는 것이다.
public class ListBox : IEnumerable
{
// ListBoxEnumerator를 중첩 클래스로 구현
private class ListBoxEnumerator : IEnumerator
{
// 중첩 클래스를 위한 코드
}
 // ListBox 클래스 코드
}
ListBoxEnumerator는 중첩된 클래스 안에서 ListBox 클래스를 참조해야 하므로 ListBoxEnumerator 생성자에 ListBox 클래스 참조를 전달한다.
ListBoxEnumerator 클래스에서 IEnumerator 인터페이스를 구현하기 위해 MoveNext와 Reset 메서드와 Current 속성을 정의해야 한다. 메서드와 속성이 하는 일은 ListBox의 요소들에 대해서 언제든지 알 수 있게 해주는 것, 현재 요소를 아는 것, 요소를 가져오는 것이 가능한 상태 머신을 만드는 것이다.
여기서 상태 머신은 문자열이 현재 문자열인 것을 가리키는 인덱스 값을 유지하며, 외부 클래스의 문자열 컬렉션을 인덱싱하여 현재 문자열을 반환한다. 이를 위해 외부 ListBox 객체에 대한 참조를 유지하기 위한 멤버 변수와 현재 인덱스를 가리키는 정수가 필요하다.
private ListBox lbt;
private int index;
Reset이 호출 될 때마다 인덱스를 -1로 설정한다.
public void Reset()
{
index = -1;
}
MoveNext가 호출될 때 마다 외부 클래스에 있는 배열은 배열의 범위를 벗어나는지 확인한다. 배열의 범위를 벗어나면 false를 반환하고, 그렇지 않은 경우에는 인덱스를 증가시키고 true를 반환한다.
public bool MoveNext()
{
index++;
if (index >= lbt.strings.Length)
{
return false;
}
else
{
return true;
}
}
마지막으로 MoveNext가 true를 반환하면 foreach 루프에서 Current 속성을 호출한다. ListBoxEnumerator의 Current 속성은 외부 클래스 ListBox의 컬렉션 인덱스를 의미하며, ListBox 컬렉션에 있는 객체를 반환하다.(여기서는 문자열) Current 속성의 반환 형식은 IEnumerator 인터페이스에 정의된 object 형식으로 반환하는 것에 주의한다.
public object Current
{
get
{
return(lbt[index]);
}
}
C# 1.1에서는 클래스가 foreach 루프를 수행하기 위해 IEnumerable 인터페이스를 구현해야하며, IEnumerator를 구현한 클래스를 생성해야 한다. 더 끔찍한 것은 enumerator에 의한 반환 값에 대해서는 형 안정성이 보장되지 않는다. Current 속성은 object 형식을 반환한다. 이는 foreach 루프에서 원하는 형식을 정확하게 반환해야 한다는 것을 의미한다.
C# 2.0
C# 2.0에는 5월에 말끔히 녹아 내리는 눈처럼 이러한 문제들을 해결한다. 이 예제의 2.0 버전은 C# 2.0의 새로운 특징인 Generics와 Iterators를 사용한다.
먼저 문자열을 위해 IEnumerable을 구현하기 위해 ListBox 클래스를 다시 정의한다.
public class ListBox : IEnumerable
{
이 정의는 클래스를 foreach 루프에서 사용할 수 있으며, 루프를 반복하면서 반환되는 값이 string 형식이라는 것을 의미한다.
이전 예제에서 작성했던 중첩 클래스를 정리하고, GetEnumerator 메서드를 다음과 같이 정리한다.
public IEnumerator GetEnumerator()
{
foreach (string s in strings)
{
yield return s;
}
}
GetEnumerator 메서드는 새로운 yield 문을 사용한다. yield 문은 표현식을 반환하다. yield 문은 iterator 블록 안에서만 나타나며, foreach 문을 호출할 때 예상되는 값을 반환하다. 즉, GetEnumerator를 호출할 때마다 컬렉션에 있는 다음 문자열을 내보낸다. 이것으로 컬렉션의 상태 관리가 끝난다.
각 데이터 형식에 대해서 enumerator를 구현할 필요도 없으며, 중첩 클래스를 만들 필요도 없다. 게다가 코드가 30줄 정도 줄어들 뿐 아니라 보다 간단해진다. 프로그램은 여전히 의도한 대로 돌아갈 것이며 상태 관리도 신경쓰지 않아도 된다. 게다가 iterator가 반환하는 값도 string 형식이라는 것을 보증한다. 만약, 다른 형식을 반환하기로 했다면 IEnumerable에서 제네릭 문을 수정하고, 새로운 형식을 지원하다록 IEnumeratoe 제네릭 문을 수정하면 된다.
yield에 관하여
위에서 설명한 것 외에 알아두면 좋은 것이 있는데, yield는 한 번에 여러개의 값을 돌려줄 수 있다. 따라서, 다음도 올바른 코드다.
public IEnumerator GetEnumerator()
{
yield return "Who";
yield return " is";
yield return "John Galt?";
}
위 코드가 클래스 foo에 있다고 가정하고, 다음과 같은 코드를 작성했다고 하자.
foreach ( string s in new foo )
{
Console.Write(s);
}
결과는 다음과 같다:
Who is John Galt?
잠시 생각해 보면 위 코드도 잘 수행된다는 것을 이해할 것이다. yield는 자체의 foreach 루프를 수행하고, 발견한 각 문자열을 모두 산출해낸다.
Jesse Liberty는 컴퓨터 컨설턴르, 강사, .NET과 웹 개발에 관한 도서의 저자이기도 하다. Jesse는 그의 책에 대한 지원을 그의 웹 사이트
www.LibertyAssociates.com에서 제공하고 있다.