CodeGym /행동 /C# SELF /열거 가능성 계약: IEnumerable<T>

열거 가능성 계약: IEnumerable<T>

C# SELF
레벨 28, 레슨 1
사용 가능

1. 소개

아마 이미 눈치챘겠지만, C#의 여러 컬렉션들은 비슷한 점이 있어. 예를 들어, 거의 모든 컬렉션을 foreach 루프로 돌릴 수 있지:


var names = new List<string> { "아냐", "보리스", "비카" };

// 주의 — 이 코드는 List, 배열, 심지어 HashSet에도 잘 동작해!
foreach (var name in names)
{
    Console.WriteLine(name);
}

이게 어떻게 가능할까? 그건 모든 최신 컬렉션들이 IEnumerable<T> 인터페이스를 구현하기 때문이야. 이건 일종의 "계약"이야. 즉, 컬렉션이 하나씩 순서대로 요소를 반환할 수 있다는 걸 보장해주는 거지.

회사에 있다고 상상해봐. 어떤 직원 컬렉션(부서, 프로젝트 팀, 회식 참가자 리스트 등)이든 "IEnumerable" 인터페이스를 구현하면, 그 안에 직원들이 어떻게 저장되어 있든 상관없이 항상 정해진 순서대로 전부 돌 수 있다는 규칙이 있는 거야.

2. IEnumerable<T> 인터페이스 — 내부는?

.NET에서 이 인터페이스가 어떻게 생겼는지 보자:


public interface IEnumerable<out T>
{
    IEnumerator<T> GetEnumerator();
}

딱 한 개의 메서드만 있어 — GetEnumerator. 이 메서드는 IEnumerator<T> 타입의 객체를 반환해. 이 객체가 바로 "열거" 과정을 담당하지. 지금 어떤 요소에 있는지, 다음으로 어떻게 가는지, 끝났는지 등을 알고 있어.

요약: 만약 클래스(혹은 컬렉션)가 IEnumerable<T>를 구현하면, foreach 루프로 돌 수 있고, 요소를 하나씩 받을 수 있어. 내부적으로 어떻게 구현됐는지는 상관없어. 리스트든, 해시테이블이든, 심지어 하드디스크에 있든!

간단 도식


컬렉션 (예: List<T>)
    |
    v
IEnumerable<T>
    |
    v
IEnumerator<T> (진짜 "요소 순회자")

3. 실습과 코드의 범용성

이런 계약의 최고 장점은 바로 범용성이야. 함수나 메서드가 IEnumerable<T>를 받는다면, 어떤 컬렉션 타입이든 다 호환돼 — 배열, 리스트, 해시셋, 큐, 심지어 네가 만든 컬렉션까지!

예를 들어, 요소들의 합을 구하는 함수를 보자:


// IEnumerable<int>를 지원하는 어떤 int 집합이든 합을 구할 수 있어
int Sum(IEnumerable<int> numbers)
{
    int result = 0;
    foreach (var n in numbers)
        result += n;
    return result;
}

// List<int>에도 동작
var list = new List<int> { 1, 2, 3 };
Console.WriteLine(Sum(list));

// 배열에도 동작!
int[] array = { 4, 5, 6 };
Console.WriteLine(Sum(array));

// 필터링 메서드 결과에도 동작
Console.WriteLine(Sum(list.Where(x => x % 2 == 0))); // LINQ 사용

실생활 예시: 범용 메서드

예를 들어, 어떤 문자열 컬렉션에서 가장 긴 단어를 찾는 유틸리티를 만들어야 한다고 해보자. IEnumerable<string> 덕분에, 배열이든, 리스트든, 필터링 메서드 결과든 다 쓸 수 있어:


string FindLongest(IEnumerable<string> words)
{
    string longest = "";
    foreach (var word in words)
        if (word.Length > longest.Length)
            longest = word;
    return longest;
}

아무 컬렉션이나 다 쓸 수 있어.

4. 왜 foreach 루프가 IEnumerable<T>에서 동작할까?

자주 나오는 질문: 왜 foreach 루프가 아무 컬렉션이나 "이해"할까? 답은 간단해: C# 컴파일러가 클래스에서 GetEnumerator 메서드를 찾고, MoveNext() 메서드와 Current 프로퍼티가 있는 객체를 기대하기 때문이야. 이게 바로 표준 인터페이스 — IEnumerator<T>야.

이런 구조 덕분에, 네가 직접 요소를 하나씩 "내놓는" 클래스를 만들어도(예: 피보나치 수열 생성기), IEnumerable<int>만 구현하면 foreach에서 리스트처럼 쓸 수 있어.

5. Enumerator란? 어떻게 동작할까?

IEnumerable<T>를 구현하는 모든 컬렉션 내부에는 특별한 “순회자” — Enumerator(공식적으로는 IEnumerator<T> 인터페이스 구현 객체)가 숨어 있어. 바로 이 녀석이 foreach 루프에서 요소를 하나씩 "꺼내" 주는 거지.

IEnumerator<T> 인터페이스

표준 Enumerator가 할 수 있는 일:


public interface IEnumerator<T> : IDisposable
{
    T Current { get; }         // 현재 요소
    bool MoveNext();           // 다음 요소로 이동
    void Reset();              // 처음으로 리셋 (거의 안 씀)
}

내부적으로 어떻게 동작할까?

  • MoveNext() — "포인터"를 다음 요소로 옮기고, 요소가 있으면 true를 반환해. 더 이상 요소가 없으면 false를 반환.
  • Current — 현재 요소(지금 순회자가 가리키는 것)를 반환.
  • Reset() — Enumerator를 처음으로 리셋(거의 안 씀).
  • Dispose() — 리소스 해제(파일이나 네트워크 등 외부 리소스를 쓰는 컬렉션에 필요).

예시: foreach가 "속에서" 어떻게 동작하는지

네가 이렇게 쓴다면:


var numbers = new List<int> { 1, 2, 3 };
foreach (var n in numbers)
    Console.WriteLine(n);

사실 컴파일러는 이걸 대충 이렇게 바꿔:


var numbers = new List<int> { 1, 2, 3 };

// "순회자"를 얻어옴
var enumerator = numbers.GetEnumerator();
while (enumerator.MoveNext())
{
    var n = enumerator.Current;
    Console.WriteLine(n);
}
// enumerator가 IDisposable을 구현하면 컴파일러가 using 블록에서 Dispose()를 자동 호출해줌

중요: 컬렉션이 외부 리소스(예: 파일, DB 등)를 쓴다면, Enumerator가 순회 끝날 때 자동으로 해제해줄 수 있어.

시각적 도식


순회 시작 -> GetEnumerator() -> Enumerator
         |
         v
  MoveNext() -> Current
         |
         v
  MoveNext() -> Current
         |
        ...
         |
         v
  MoveNext() == false -> 순회 끝

6. IEnumerable과 배열, 리스트, 집합: 누가 누구 친척?

.NET의 표준 컨테이너 중 어떤 것들이 이 인터페이스를 구현하는지 보자:

컬렉션 타입 IEnumerable<T> 구현? foreach로 순회 가능?
배열 (
int[]
)
✅ ✅
List<T>
✅ ✅
Dictionary<TKey, V>
✅ (쌍, 키, 값 기준) ✅
HashSet<T>
✅ ✅
Queue<T>
✅ ✅
Stack<T>
✅ ✅

심지어 문자열(string)도 일반 IEnumerable을 구현해서, 문자열의 각 문자도 똑같이 순회할 수 있어.

7. 나만의 Enumerable 구현하기

재밌는 과제: 0부터 N까지 짝수만 저장하는 최소한의 컬렉션을 만들어서 IEnumerable<int>를 구현해보자. 그러면 foreach로 돌릴 수도 있고 LINQ도 쓸 수 있어.


// "순회" 가능한 컬렉션 클래스
class EvenNumbers : IEnumerable<int>
{
    private int max;

    public EvenNumbers(int max)
    {
        this.max = max;
    }

    public IEnumerator<int> GetEnumerator()
    {
        for (int i = 0; i <= max; i += 2)
            yield return i; // enumerator 구현을 위한 특별한 마법
    }

    // 구버전 IEnumerable 인터페이스 명시적 구현 (호환성용)
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

// 사용 예시:
var evens = new EvenNumbers(10);
foreach(var e in evens)
    Console.Write($"{e} "); // 0 2 4 6 8 10

중요 포인트: 네 클래스가 IEnumerable<T>를 구현하면, .NET의 대부분 도구들과 자동으로 호환돼: LINQ, foreach, enumerable을 받는 메서드 등등.

8. 흔한 실수와 주의점

초보 개발자들이 IEnumerable<T>가 뭔가 별도의 컬렉션이고, 요소가 있다고 착각하는 경우가 있어. 사실 이건 그냥 "약속"일 뿐이야. 순회를 시작하면 요소를 하나씩 "내놓겠다"는 거지.

인덱스로 임의 접근(myList[5])이나 Add, Remove 같은 메서드를 쓰고 싶으면 IEnumerable<T>로는 안 돼. 이건 오직 순차적 순회용이야!

실수: 순회 중에 컬렉션을 수정하려고 하는 것. 예를 들어:


foreach (var item in myList)
{
    if (item < 0)
        myList.Remove(item); // 위험! InvalidOperationException
}

더 안전한 방법은 삭제할 요소를 따로 리스트로 모으거나, 새로운 컬렉션을 만드는 거야:


// 안전한 방법 — 새 컬렉션을 수동으로 만듦
var newList = new List<int>();
foreach (var item in myList)
{
    if (item >= 0)
        newList.Add(item);
}
myList = newList;

// 또는 인덱스를 거꾸로 돌면서 삭제
for (int i = myList.Count - 1; i >= 0; i--)
{
    if (myList[i] < 0)
        myList.RemoveAt(i);
}
2
과제
C# SELF, 레벨 28, 레슨 1
잠금
IEnumerable를 지원하는 직접 만든 컬렉션 구현하기
IEnumerable를 지원하는 직접 만든 컬렉션 구현하기
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION