
<T>지난 글에서 다룬 C# 인덱서(Indexer)편에서 여러분은 혹시 이런 생각해 보셨나요?
// 이 코드는 왜 안될까?
foreach (var student in myGrades)
{
Console.WriteLine($"{student.Key}: {student.Value}");
}
for문으로는 순회할 수 있겠지만, C#의 우아한 반복문인 foreach를 사용할 수 없다니!
마치 열쇠가 없어 들어가지 못하는 보물창고를 보는 기분입니다.
이럴 때 우리는 어떻게 해야 할까요? 그 비밀은 바로
IEnumerable<T>라는 마법의 열쇠, 즉 인터페이스에 있습니다.
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);
}
<T> 파헤치기IEnumerable<T>인터페이스는 놀랍도록 간단합니다.
이 인터페이스를 구현한다는 것은 단 하나의 메서드를 만드는 것을 의미합니다.
public interface IEnumerable<T> : IEnumerable
{
// "기관사(IEnumerator<T>)를 반환해 주세요"
IEnumerator<T> GetEnumerator();
}
GetEnumerator(): '기관사'에 해당하는 IEnumerator<T>객체를 반환하는 메서드입니다.IEnumerator<T>: 실제 순회 작업을 담당하는 객체로,MoveNext()메서드와 Current속성을 가지고 있습니다.IEnumerable(제네릭이 아닌 버전)도 함께 상속받는데, 이는 하위 호환성을 위한 것입니다.
이제 우리의 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딕셔너리의 모든 요소를 성공적으로 순회했습니다!
foreach문은 단순한 반복문이 아니라, 체계적으로 구현된 시스템입니다.
이제 여러분이 직접 만든 컬렉션 클래스가 있다면,
주저하지 말고 IEnumerable<T>인터페이스를 구현해 보세요.
foreach문으로 클래스의 활용성을 한 단계 더 높일 수 있을 것입니다!
| 개념 | 역할 | 비유 |
|---|---|---|
foreach | 컬렉션의 모든 요소를 순회하는 문법 | 기차 여행객 |
IEnumerable<T> | 순회할 수 있음을 보장하는 '약속' | 철도 노선도 |
GetEnumerator() | 순회 로직을 담은 객체를 반환하는 '메서드' | 기관사 배정 창구 |
IEnumerator<T> | 실제 순회를 담당하는 '객체' | 기관사 |