.NET에서 제공하는 IEnumerator
인터페이스를 구현한 객체를 Enumerator(열거자)라고 한다.
System.Collections
에 있는 컬렉션들은 IEnumerable
을 상속받아 구현되어 있다.
따라서 List
, Array
등의 컬렉션 클래스는 foreach
문을 사용할 수 있다.
namespace System.Collections
{
public interface IEnumerator
{
object Current { get; }
bool MoveNext();
void Reset();
}
}
IEnumerator는 컬렉션을 반복할 때 사용한다.
Current
읽기 전용 프로퍼티MoveNext()
메서드Reset()
메서드namespace System.Collections
{
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
}
IEnumerable
에 선언된 GetEnumerator()
메서드를 통해 IEnumerator
를 반환한다.
추가적으로 IEnumertor
또는 IEnumerable
을 반환하는 모든 메서드는 ref
, out
키워드 사용이 불가하다. 또한 람다 함수에 사용할 수도 없다.
또한 제네릭을 사용해서 열거하는 값의 타입을 지정해줄 수 있다. 타입을 지정안하면 object 타입으로 넘어가 박싱/언박싱이 일어나게 되어 조심할 필요가 있다.
foreach 순회는 아래 3단계를 따른다.
GetEnumerator()
메서드를 호출하여 IEnumerator
객체를 얻는다.MoveNext()
를 먼저 호출하여 다음 요소로 이동한 후, Current
를 반환한다.MoveNext()
가 false
를 반환할 때까지 순회한다.즉, foreach
문을 사용하려면 다음 조건을 만족해야 한다.
순회하려는 객체는 GetEnumerator()
메서드를 가지고 있어야 한다.
IEnumerable
을 상속하면 기본적으로 이 조건을 만족한다. IEnumerable
을 상속하지 않더라도 GetEnumerator()
를 직접 구현하면 foreach
사용 가능하다.GetEnumerator()
의 반환 타입은 Current
프로퍼티(읽기 전용)와 MoveNext()
메서드를 가지고 있어야 한다.
IEnumerator
인터페이스를 구현해야 한다.이를 구현하면 아래와 같은 코드로 foreach
문이 작동한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
class Enumerator : IEnumerable, IEnumerator
{
private int[] items = { 1, 2, 3, 4, 5 };
private int index = -1;
public object Current { get { return items[index]; } }
public IEnumerator GetEnumerator()
{
Reset();
return this;
}
public bool MoveNext()
{
index++;
return items.Length > index;
}
public void Reset()
{
index = -1;
}
}
public class Practice : MonoBehaviour
{
void Start()
{
Enumerator enumerator = new Enumerator();
foreach (var item in enumerator)
{
Debug.Log(item);
}
}
}
추가적으로 foreach
문은 아래와 같은 코드로 치환 가능하다.
void Start()
{
IEnumerable enumerable = new Enumerator();
IEnumerator enumerator = enumerable.GetEnumerator();
while (enumerator.MoveNext())
{
int current = (int)enumerator.Current;
Debug.Log(current);
}
}
이렇게 IEnumerator와 IEnumerable 인터페이스를 활용하여 컬렉션의 내부 구조를 몰라도 순회할 수 있게 해주는 디자인 패턴을 '반복자(iterator) 패턴' 이라고 한다.
지금까지 설명한 예제 코드들은 설명의 편의성을 위해 제네릭을 사용하지 않았지만, 실제로는 IEnumerator<T>
, IEnumerable<T>
와 같은 제네릭 타입을 사용하는 것이 일반적이다. 그렇지 않으면 값 타입을 순회할 때 박싱(Boxing)이 발생해 추가적인 성능 비용이 발생할 수 있다.
추가적으로 유의해야할 점은 foreach 문을 사용할 때마다 IEnumerator 객체가 생성되기 때문에, 과도하게 사용할 경우 성능 저하로 이어질 수 있다는 것이다. (단, 예제 코드처럼 this를 반환하는 특수 케이스는 성능 문제 없음)
C#에서 yield
키워드를 사용하면 C# 컴파일러는 해당 메서드를 반복자 패턴으로 변환하여 IEnumerable
또는 IEnumerator
인터페이스를 구현하는 코드를 생성한다. 이를 통해 yield return
을 호출할 때마다 상태를 저장하고, 다음 호출에서 중단된 위치부터 실행을 재개할 수 있도록 한다.
class Enumerator
{
private int[] items = {1,2,3,4,5 };
public IEnumerator GetEnumerator()
{
yield return items[0];
yield return items[1];
yield return items[2];
yield return items[3];
yield return items[4];
}
}
public class Practice : MonoBehaviour
{
void Start()
{
Enumerator enumerator = new Enumerator();
foreach(var item in enumerator)
{
Debug.Log(item);
}
}
}
이렇게 yield return
을 사용하여 한 번 호출될 때마다 하나씩 값을 반환하고, 지연 호출이 가능해진다.
(참고로, 컴파일러는 이러한 yield 메서드를 자동으로 상태 머신(state machine) 클래스로 변환해준다고 한다.)
지금까지 IEnumerator, IEnumerable 그리고 foreach문이 동작하는 방식과, yield 키워드를 활용한 반복자 패턴에 대해 살펴보았다. 핵심은 컬렉션의 내부 구조를 몰라도 일관된 방식으로 요소들을 순회할 수 있도록 해주는 것이 반복자 패턴의 목적이라는 점이다.
결론적으로, 반복자 패턴은
1. 코드의 가독성과 유지보수성을 크게 높여주지만,
2. 메모리 할당과 성능 측면에서는 신중하게 사용할 필요가 있다.
"언제 사용하고, 언제 주의할 것인가?" 에 대한 감각을 익히는 것이 중요한 것 같다.