C# 에서 List<T> 나 Dictionary<TKey, TValue> 처럼 컬렉션을 다룰 때, 단순히 foreach() 로 순회하는 것 이상으로 이해하고 싶다면 IEnumerable<T>, ICollection<T> 같은 인터페이스 구조를 짚고 넘어가야 합니다.
( 참고로 이 글은 interface 개념을 어느 정도 이해하고 있다는 전제하에 작성되었습니다. )
foreach() 는 어떻게 동작할까?
다음과 같은 foreach() 문이 있다고 가정해 보겠습니다.
Dictionary<string, int> fruitPrices = new Dictionary<string, int>()
{
{ "사과", 1500 },
{ "바나나", 1200 },
{ "오렌지", 1800 },
{ "포도", 2000 },
{ "딸기", 3000 }
};
foreach (var pair in fruitPrices)
{
Console.WriteLine($"{pair.Key} : {pair.Value}");
}
이 코드는 내부적으로 다음과 같은 형태로 컴파일러가 해석합니다.
var enumerator = fruitPrices.GetEnumerator();
while (enumerator.MoveNext())
{
var pair = enumerator.Current;
Console.WriteLine($"{pair.Key} : {pair.Value}");
}
foreach() 는 컬렉션에서 GetEnumerator() 를 호출하고, 내부적으로 MoveNext()와 Current를 이용해 순회합니다. 이 구조를 지원하려면 해당 컬렉션은 반드시 IEnumerable<T> 인터페이스를 구현해야 합니다.
IEnumerable<T> 와 IEnumerator<T>
아래는 IEnumerable<T> 의 정의입니다.
public interface IEnumerable<out T>
{
IEnumerator<T> GetEnumerator();
}
그리고 IEnumerator<T> 는 반복을 위한 핵심 구성요소를 포함합니다.
public interface IEnumerator<T>
{
T Current { get; }
bool MoveNext();
void Reset(); // 일반적으로는 생략됨
}
이 두가지 인터페이스만 구현하면, 어떤 클래스든 foreach() 문을 사용할 수 있게 됩니다. 실제로 모든 컬렉션들은 내부적으로 이 구조를 각 상황에 맞게 구현하고 있습니다.
ICollection<T> 의 역할
ICollection<T>는 IEnumerable<T>를 상속하며, 다음과 같은 기능들을 추가합니다.
public interface ICollection<T> : IEnumerable<T>
{
int Count { get; }
void Add(T item);
bool Remove(T item);
void Clear();
bool Contains(T item);
}
즉, 요소를 추가 (Add) 하고 삭제 (Remove) 하며, 개수 (Count) 확인 등의 기능을 제공합니다.
ICollection<T>, IEnumerable<T>
ICollection<T>, IEnumerable<T> 인터페이스는 컬렉션의 핵심이기 때문에 다음처럼 구현하고 있습니다.
public class List<T> : ICollection<T>, IEnumerable<T>
{
// ...
}
public class Dictionary<TKey, TValue> : IDictionary<TKey, TValue>,
ICollection<KeyValuePair<TKey, TValue>>,
IEnumerable<KeyValuePair<TKey, TValue>>
{
// ...
}
또한, Dictionary 에서 Dictionary<TKey, TValue>.Keys 와 Dictionary<TKey, TValue>.Values 속성이 존재하는데, 이것은 각각 Key 값과 Value 값들을 모아놓은 또 하나의 독립된 컬렉션 (Collection) 입니다.
public class Dictionary<TKey, TValue>.KeyCollection : ICollection<TKey>,
IEnumerable<TKey>
{
// ...
}
public class Dictionary<TKey, TValue>.ValueCollection : ICollection<TValue>,
IEnumerable<TValue>
{
// ...
}
Dictionary<Tkey, TValue> 의 Key 목록을 List<T> 로 변환하기
Dictionary<TKey, TValue>.Keys는 ICollection<TKey>이므로 IEnumerable<TKey>처럼 사용할 수 있습니다.
따라서 다음과 같이 리스트로 변환할 수 있습니다.
// List<T>(IEnumerable<T> collection) 생성자 호출
List<string> keys1 = new List<string>(fruitPrices.Keys);
코드를 살펴보면, Dictionary<TKey, TValue>.Keys 는 ICollection<TKey> 를 구현합니다.
ICollection<T> 는 IEnumerable<T> 을 상속하므로, Keys 는 IEnumerable<T> 처럼 사용할 수 있습니다.
따라서 List<string>(fruitPrices.Keys) 와 같이, List<string>( IEnumerable<T>) 생성자에 매개 변수로 전달할 수 있습니다. 그러면 내부적으로 IEnumerable<T> 를 List<T> 형태로 변환하여 돌려줍니다.
참고로 다음과 같이 LINQ 확장 메서드를 사용해도 됩니다.
( 사실 LINQ 는 아직 깊이 있게 이해하고 있지 못해서 설명이 부족합니다. )
// LINQ 사용
List<string> keys2 = fruitPrices.Keys.ToList();
Key 리스트를 이용한 foreach() 문에서의 순회
// 생성자에서 IEnumerable<string> 받아 리스트 생성
List<string> keys = new List<string>(fruitPrices.Keys);
foreach (var key in keys)
{
fruitPrices[key] += 100;
}
fruitPrices.Keys 를 List<T> 형태로 만들어서, List<string> keys 에 복사 (참조 복사) 해주었기 때문에, foreach() 문에서 keys 를 순회할 수 있습니다. 참고로 foreach() 문 안에서 순회를 할 경우, 컬렉션의 크기 변경은 금지되고, 값 수정은 얼마든지 가능합니다. 따라서 위와 같이 key 값을 따로 복사해서 순회할 필요는 없지만, 예시를 위해 작성된 코드입니다.
정리
• foreach() 문은 내부적으로 GetEnumerator(), MoveNext(), Current를 사용하는 구조이며, 이를 위해 컬렉션은 IEnumerable<T>를 구현해야 합니다.
• ICollection<T>는 Add, Remove, Count 등을 지원하여 컬렉션을 조작할 수 있게 합니다.
• List<T> 생성자는 IEnumerable<T>를 매개 변수로 받아, 리스트 형태로 만들어 줍니다.
• Dictionary<TKey, TValue>.Keys 는 ICollection<T>를 구현하고 있으며, ICollection<T> 는 IEnumerable<T>를 상속하므로, Keys 역시 IEnumerable<T>처럼 사용할 수 있습니다.
'C#' 카테고리의 다른 글
Dictionary 에서 구조체를 TValue 로 사용시, 주의할 점 (1) | 2025.07.24 |
---|---|
Dictionary 정리 : 기초부터 활용까지 (0) | 2025.07.23 |
구조체를 List<T> 에 추가하는 방법들 (0) | 2025.07.19 |
foreach() 가 내부적으로 동작하는 방식 (0) | 2025.07.15 |
Sort() 를 보다 깊이있게 이해하기 (0) | 2025.07.10 |