Array IEnumerator

황현중·2025년 11월 21일

C#

목록 보기
8/24
C# 배열(Array)과 IEnumerator 완전 정리

C# 배열(Array)과 IEnumerator 완전 이해하기


1. 먼저 큰 그림: IEnumerable & IEnumerator란 무엇인가

1-1. IEnumerable / IEnumerable<T>

IEnumerable"열거할 수 있는 것"을 의미하는 인터페이스이다
가장 중요한 멤버는 GetEnumerator() 메서드이다


// 비제네릭
public interface IEnumerable
{
    IEnumerator GetEnumerator();
}

// 제네릭
public interface IEnumerable<out T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
}
  

어떤 타입이든 IEnumerable 또는 IEnumerable<T>를 구현하고 있으면 foreach로 돌릴 수 있다는 의미이다

1-2. IEnumerator / IEnumerator<T>

IEnumerator"현재 위치를 기억하면서 한 칸씩 이동하는 열거자"를 의미하는 인터페이스이다
핵심 멤버는 다음과 같다


// 비제네릭
public interface IEnumerator
{
    bool MoveNext();    // 다음 요소로 이동, 이동 성공 여부 반환
    object Current { get; } // 현재 요소
    void Reset();       // 처음 위치로 리셋 (요즘은 잘 안씀)
}

// 제네릭
public interface IEnumerator<out T> : IDisposable, IEnumerator
{
    new T Current { get; }
}
  

정리하면 다음과 같다

  • IEnumerable = 열거자를 만들어주는 쪽이다
  • IEnumerator = 실제로 한 칸씩 움직이며 데이터에 접근하는 객체이다
중요 개념
foreach는 결국 내부적으로 IEnumerable.GetEnumerator()IEnumerator를 하나 얻고, 그 열거자를 가지고 MoveNext()Current를 반복 호출하는 패턴일 뿐이다

2. 배열(Array)은 IEnumerable를 구현하고 있다

C#의 모든 1차원 배열은 다음 인터페이스들을 구현하고 있다

  • IEnumerable
  • IEnumerable<T>
  • ICollection, IList 등도 구현하지만 여기서는 생략한다

예를 들어 int[] 배열이 있다고 하면, 이 배열은 IEnumerable<int>이다


int[] numbers = { 10, 20, 30 };

// 명시적으로 캐스팅 가능
IEnumerable enumerable = numbers;
IEnumerable<int> genericEnumerable = numbers;

// 물론 foreach는 바로 사용 가능
foreach (var n in numbers)
{
    Console.WriteLine(n);
}
  

배열도 "foreach가 가능한 컬렉션"이라는 점에서는 List와 완전히 동일한 개념이다
단, 구현 방식은 조금 다르며, 배열은 CLR 레벨에서 특별 취급되는 타입이라는 점이 다를 뿐이다


3. foreach가 배열에서 실제로 하는 일 (컴파일 후 모습)

3-1. foreach의 표면적인 코드


int[] numbers = { 1, 2, 3, 4, 5 };

foreach (int n in numbers)
{
    Console.WriteLine(n);
}
  

이 코드는 우리가 익숙하게 쓰는 형태이다
하지만 컴파일러는 이 코드를 다음과 비슷한 형태로 변환한다

3-2. 비슷한 형태로 풀어 쓴 코드


int[] numbers = { 1, 2, 3, 4, 5 };

// 1. GetEnumerator 호출
IEnumerator<int> enumerator = ((IEnumerable<int>)numbers).GetEnumerator();

try
{
    // 2. MoveNext로 한 칸씩 이동
    while (enumerator.MoveNext())
    {
        // 3. Current로 현재 요소 읽기
        int n = enumerator.Current;
        Console.WriteLine(n);
    }
}
finally
{
    // 4. IDisposable이면 Dispose 호출
    if (enumerator is IDisposable d)
    {
        d.Dispose();
    }
}
  

실제 실제로는 JIT 최적화 때문에 배열의 경우 이런 형태로 최적화된 for 루프로 바뀌는 경우도 많지만, foreach의 개념 상으로 보면 위 패턴을 따르고 있다고 이해하면 된다

포인트
foreach는 "열거자 패턴"을 자동으로 만들어주는 문법 설탕(syntactic sugar)일 뿐이라는 점을 이해하면 된다

4. 배열에서 IEnumerator 직접 사용해보기

4-1. 비제네릭 IEnumerator 사용


int[] numbers = { 10, 20, 30 };

IEnumerator enumerator = numbers.GetEnumerator();

while (enumerator.MoveNext())
{
    // Current는 object 타입이라 캐스팅 필요
    int value = (int)enumerator.Current;
    Console.WriteLine(value);
}
  

여기서 numbers.GetEnumerator()비제네릭 IEnumerator를 반환한다
그래서 Currentobject(int) 캐스팅이 필요하다

4-2. 제네릭 IEnumerator<T>로 다루기


int[] numbers = { 10, 20, 30 };

// IEnumerable<int>로 캐스팅해서 제네릭 열거자 얻기
IEnumerable<int> seq = numbers;
IEnumerator<int> enumerator = seq.GetEnumerator();

while (enumerator.MoveNext())
{
    int value = enumerator.Current; // 캐스팅 불필요
    Console.WriteLine(value);
}

enumerator.Dispose(); // using 또는 try-finally로 감싸는 것이 정석이다
  

제네릭 버전을 쓰면 Currentint


5. 배열 전용 foreach 최적화와 for 루프 비교

배열은 CLR 레벨에서 특별취급을 받기 때문에, JIT 컴파일러가 foreachfor 루프로 최적화해버리는 경우가 많다
그래서 다음 두 코드는 보통 성능 차이가 거의 없거나 미미한 수준이다

5-1. foreach 버전


int[] numbers = { 1, 2, 3, 4, 5 };

foreach (int n in numbers)
{
// 작업
}

5-2. for 버전


int[] numbers = { 1, 2, 3, 4, 5 };

for (int i = 0; i < numbers.Length; i++)
{
int n = numbers[i];
// 작업
}

배열만 놓고 보면 가독성 측면에서는 foreach가 더 깔끔하다는 장점이 있다
인덱스가 필요한 경우에는 for를 사용하면 된다


6. 커스텀 컬렉션에서 배열 + IEnumerator 패턴 구현하기

실무에서는 내부적으로 배열을 들고 있으면서 직접 열거 가능한 컬렉션을 만드는 경우가 많다
이때 IEnumerable<T> / IEnumerator<T>를 어떻게 구현하는지 예를 보자


public class IntCollection : IEnumerable<int>
{
    private int[] _items;

    public IntCollection(int[] items)
    {
        _items = items;
    }

    // IEnumerable<int> 구현
    public IEnumerator<int> GetEnumerator()
    {
        // 배열의 열거자를 그대로 반환해도 되고
        // yield return으로 커스텀 열거자를 만들어도 된다
        foreach (var item in _items)
        {
            yield return item;
        }
    }

    // IEnumerable (비제네릭) 구현
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator(); // 제네릭 버전 재사용
    }
}

// 사용 예
var collection = new IntCollection(new[] { 1, 2, 3 });

foreach (var n in collection)
{
    Console.WriteLine(n);
}
  

위 예제에서 yield return을 사용하면 컴파일러가 내부적으로 IEnumerator<int>를 구현한 상태 머신 클래스를 자동으로 생성해준다
덕분에 직접 MoveNext, Current를 구현할 필요 없이 간단하게 열거자 패턴을 만들 수 있다


7. IEnumerator / 배열 관련 실무에서 자주 하는 실수

7-1. Current를 범위 밖에서 읽으려는 경우


int[] numbers = { 1, 2, 3 };
var e = ((IEnumerable<int>)numbers).GetEnumerator();

// MoveNext 호출 없이 Current 접근 (잘못된 코드)
int value = e.Current; // 정의되지 않은 동작, InvalidOperationException 가능
  

규칙은 다음과 같다

  • MoveNext()최초 한 번 호출한 뒤Current
  • MoveNext()false를 반환하여 열거가 끝난 뒤에는 Current

7-2. 컬렉션을 수정하면서 열거하기

일반적인 컬렉션(List 등)은 foreach로 열거 중에 컬렉션을 수정하면 예외가 발생한다
배열의 길이는 고정이라 조금 다른 성격이지만, 기본 원칙은 열거 중에는 컬렉션 구조를 바꾸지 않는 것이 안전하다

7-3. IDisposable 처리 잊기

IEnumerator<T>IDisposable을 상속받기 때문에, 직접 사용할 때는 using 또는 try/finallyDispose()를 호출해주는 것이 원칙이다
foreach를 사용하면 컴파일러가 자동으로 Dispose() 호출을 넣어주므로 신경 쓸 필요가 없다


IEnumerable<int> seq = new List<int> { 1, 2, 3 };

using (var e = seq.GetEnumerator())
{
    while (e.MoveNext())
    {
        int value = e.Current;
        Console.WriteLine(value);
    }
}
  

8. 정리: 배열과 IEnumerator를 이해하면 보이는 것들

  • 배열(T[])은 IEnumerable / IEnumerable<T>를 구현하고 있는 컬렉션이다
  • foreach는 결국 GetEnumerator(), MoveNext(), Current를 자동으로 호출해주는 문법 설탕이다
  • 배열에서도 GetEnumerator()를 직접 호출하여 IEnumerator를 꺼내 수동으로 순회할 수 있다
  • 배열의 foreach는 JIT에 의해 for 루프로 최적화되는 경우가 많아 성능 손해가 거의 없다
  • 커스텀 컬렉션을 만들 때는 내부에 배열을 두고 IEnumerable<T>/IEnumerator<T> 패턴을 구현하면 foreach와 완벽히 통합된다
  • IEnumerator를 직접 사용할 때는 MoveNext() 호출 순서와 Current 사용 규칙, Dispose() 처리에 주의해야 한다

0개의 댓글