[로봇활용_11주차] C# IEnumerable<T> - 컬렉션 순회

최윤호·2025년 10월 18일
post-thumbnail

마법의 열쇠: IEnumerable<T>

지난 글에서 다룬 C# 인덱서(Indexer)편에서 여러분은 혹시 이런 생각해 보셨나요?

// 이 코드는 왜 안될까?
foreach (var student in myGrades)
{
    Console.WriteLine($"{student.Key}: {student.Value}");
}

for문으로는 순회할 수 있겠지만, C#의 우아한 반복문인 foreach를 사용할 수 없다니!
마치 열쇠가 없어 들어가지 못하는 보물창고를 보는 기분입니다.
이럴 때 우리는 어떻게 해야 할까요? 그 비밀은 바로
IEnumerable<T>라는 마법의 열쇠, 즉 인터페이스에 있습니다.

1)foreach는 어떻게 작동할까?

IEnumerable<T>를 이해하려면, 먼저 foreach가 컴파일러에 의해
어떻게 번역되는지 알아야 합니다. foreach는 사실 C# 컴파일러가 제공하는
편리한 문법 설탕(Syntactic Sugar)일 뿐, 내부적으로는 조금 더 복잡한 과정으로 변환됩니다.

비유: 기차 여행

foreach문은 '기차 여행객'입니다. 이 여행객은
'철도 노선도(IEnumerable<T>)'를 보고 '기관사(IEnumerator<T>)'를 구합니다.

1. 여행객(foreach): "이 노선(myGrades)을 따라 모든 역을 방문하고 싶어요!"
2. 노선도(IEnumerable<T>): "알겠습니다. 저희 노선을 운행할 기관사
(IEnumerator<T>)를 한 명 배정해 드릴게요." (GetEnumerator() 메서드 호출)
3. 기관사(IEnumerator<T>):

  • 여행객: "다음 역으로 가주세요."
  • 기관사: "네, 다음 역으로 이동합니다. 아직 역이 남았어요." (MoveNext()true반환)
  • 여행객: "지금 도착한 역이 어디죠?"
  • 기관사: "여기는 서울역입니다." (Current 속성으로 현재 값 제공)

4. 이 과정을 마지막 역에 도착할 때까지 반복합니다.
(MoveNext()false를 반환하면 여행 종료)

즉, foreach가 어떤 컬렉션을 순회하려면, 그 컬렉션은 반드시
'기관사를 배정해 줄 수 있다는 약속(IEnumerable<T>)'을 지켜야 합니다.

[우리가 입력한 코드]

foreach (var item in cities)
{
    Console.WriteLine(item);
}

[컴파일러에 의해 변환되는 코드]

// 컴파일러가 번역한 코드 (가장 효율적인 버전)
// 1. 여행객: "이 노선을 따라 모든 역을 방문하고 싶어요!"
var enumerator = cities.GetEnumerator(); // 2. 노선을 운행할 기관사를 부른다

try
{
    while (enumerator.MoveNext()) // 3. 다음 역으로 이동 & 확인
    {
        var item = enumerator.Current; // 4. 현재 역 이름 확인
        Console.WriteLine(item);
    }
}
finally // 기차에 문제가 생겨 멈추더라도 승무원은 반드시 뒷정리(Dispose)를 합니다.
{
    // System.Collections.Generic에 포함된 List<T> 같은 제네릭 컬렉션인 경우
    // 컴파일러는 승무원(IDisposable)의 존재를 처음부터 알고 뒷정리(Dispose)를 시켜요.
    enumerator.Dispose(); // 5. 여행이 끝나면 승무원이 뒷정리!

    // 만약 컴파일러가 '승무원'의 신분증을 직접 못 보고 알고만 있을 때 아래 코드를 사용해요.
    // 런타임(실행 시점)에 "혹시 정리할 줄 아는 분(IDisposable)이신가요?"라고 물어보고,
    // 맞으면 컴파일러는 승무원(IDisposable)에게 뒷정리(Dispose)를 시켜요.
    // (enumerator as IDisposable)?.Dispose();
}

[배열은 특별 대우!]
string[], int[]등 배열은 C#에서 가장 기본적이고 구조가 단순해요.
그래서 컴파일러는 굳이 '기관사'를 부르는 복잡한 절차 대신,
우리에게 익숙한 for문로 번역해버려요. 이게 훨씬 빠르거든요!

// 배열은 아래처럼 번역됩니다.
for (int i = 0; i < arr.Length; i++)
{
    var item = arr[i];
    Console.WriteLine(item);
}

2)IEnumerable<T> 파헤치기

IEnumerable<T>인터페이스는 놀랍도록 간단합니다.
이 인터페이스를 구현한다는 것은 단 하나의 메서드를 만드는 것을 의미합니다.

public interface IEnumerable<T> : IEnumerable
{
    // "기관사(IEnumerator<T>)를 반환해 주세요"
    IEnumerator<T> GetEnumerator();
}
  • GetEnumerator(): '기관사'에 해당하는 IEnumerator<T>객체를 반환하는 메서드입니다.
  • IEnumerator<T>: 실제 순회 작업을 담당하는 객체로,
    MoveNext()메서드와 Current속성을 가지고 있습니다.

IEnumerable(제네릭이 아닌 버전)도 함께 상속받는데, 이는 하위 호환성을 위한 것입니다.

3)클래스에 마법 부여하기

이제 우리의 Gradebook클래스가 foreach와 함께 작동하도록
마법의 열쇠 IEnumerable<T>를 구현해 봅시다.

[Gradebook클래스 수정 사항]

using System.Collections; // IEnumerable을 위해 필요

// 1. 클래스 선언부에 IEnumerable<T> 인터페이스 상속 추가
// Dictionary는 KeyValuePair<TKey, TValue>를 순회하므로 T는 KeyValuePair<string, int>가 됨
public class Gradebook : IEnumerable<KeyValuePair<string, int>>
{
    private Dictionary<string, int> _scores = new Dictionary<string, int>();

    public int this[string studentName]
    {
        get { /* ... 이전 코드와 동일 ... */ }
        set { /* ... 이전 코드와 동일 ... */ }
    }
    
    // --- 여기서부터가 추가된 부분! ---

    // 2. 제네릭 버전의 GetEnumerator() 구현
    //    내부 _scores 딕셔너리의 GetEnumerator()를 그대로 호출하여 반환
    public IEnumerator<KeyValuePair<string, int>> GetEnumerator()
    {
        return _scores.GetEnumerator();
    }

    // 3. 비-제네릭 버전의 GetEnumerator() 구현 (하위 호환성)
    //    일반적으로 제네릭 버전을 호출하도록 구현
    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

[전체 코드]

using System;
using System.Collections; // IEnumerable을 위해 필요합니다.
using System.Collections.Generic; // 딕셔너리를 사용하려면 필요합니다.
using System.Linq; // ElementAt 메서드를 사용하려면 필요합니다.

// 1. 클래스 선언부에 IEnumerable<T> 인터페이스 상속 추가
// Dictionary는 KeyValuePair<TKey, TValue>를 순회하므로 T는 KeyValuePair<string, int>가 됨
public class Gradebook : IEnumerable<KeyValuePair<string, int>>
{
    // 2. 실제 데이터는 private Dictionary에 안전하게 보관 (내부 구현)
    private Dictionary<string, int> _scores = new Dictionary<string, int>();

    // 3. 인덱서(Indexer) 정의
    public int this[string studentName]
    {
        // get 접근자: "grades[이름]"으로 점수를 읽으려 할 때 호출됨
        get
        {
            // 해당 학생이 존재하지 않을 경우를 대비해 안전하게 접근
            if (_scores.TryGetValue(studentName, out int score))
            {
                return score;
            }
            else
            {
                // 학생이 없으면 0점 또는 예외를 반환할 수 있음
                Console.WriteLine($"{studentName} 학생의 정보가 없습니다.");
                return 0;
            }
        }

        // set 접근자: "grades[이름] = 점수"로 값을 할당할 때 호출됨
        set
        {
            // 'value' 키워드는 할당하려는 값(점수)을 의미
            if (value >= 0 && value <= 100)
            {
                // 학생 이름이 이미 있으면 점수 수정, 없으면 새로 추가
                _scores[studentName] = value;
            }
            else
            {
                Console.WriteLine("유효하지 않은 점수입니다 (0 ~ 100).");
            }
        }
    }

    // 4. 제네릭 버전의 GetEnumerator() 구현
    //    내부 _scores 딕셔너리의 GetEnumerator()를 그대로 호출하여 반환
    public IEnumerator<KeyValuePair<string, int>> GetEnumerator()
    {
        return _scores.GetEnumerator();
    }

    // 5. 비-제네릭 버전의 GetEnumerator() 구현 (하위 호환성)
    //    일반적으로 제네릭 버전을 호출하도록 구현
    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

class Program
{
    static void Main()
    {
        Gradebook myGrades = new Gradebook();

        myGrades["홍길동"] = 95;
        myGrades["김철수"] = 88;
        myGrades["이영희"] = 100;

        Console.WriteLine("--- 전체 학생 성적 목록 ---");
        foreach (var student in myGrades)
        {
            // 여기서 'student'의 타입은 KeyValuePair<string, int> 입니다.
            Console.WriteLine($"이름: {student.Key}, 점수: {student.Value}");
        }
    }
}

[실행 결과]

--- 전체 학생 성적 목록 ---
이름: 홍길동, 점수: 95
이름: 김철수, 점수: 88
이름: 이영희, 점수: 100

foreach문이 myGrades.GetEnumerator()를 호출하고,
반환된 '기관사'를 통해 _scores딕셔너리의 모든 요소를 성공적으로 순회했습니다!

4)정리

foreach문은 단순한 반복문이 아니라, 체계적으로 구현된 시스템입니다.
이제 여러분이 직접 만든 컬렉션 클래스가 있다면,
주저하지 말고 IEnumerable<T>인터페이스를 구현해 보세요.
foreach문으로 클래스의 활용성을 한 단계 더 높일 수 있을 것입니다!

개념역할비유
foreach컬렉션의 모든 요소를 순회하는 문법기차 여행객
IEnumerable<T>순회할 수 있음을 보장하는 '약속'철도 노선도
GetEnumerator()순회 로직을 담은 객체를 반환하는 '메서드'기관사 배정 창구
IEnumerator<T>실제 순회를 담당하는 '객체'기관사
profile
🚀 미래의 엔지니어를 꿈꾸는 훈련생의 기록 📝

0개의 댓글