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();
}
たった1つのメソッドしかない — GetEnumerator。これがIEnumerator<T>型のオブジェクトを返す。このオブジェクトが「列挙」そのものを担当してて、今どの要素か、次にどう進むか、もう終わったかを知ってる。
ざっくり言うと: クラス(やコレクション)がIEnumerable<T>を実装してたら、foreachループで回せるし、要素を一つずつ取り出せる。中身がリストでもハッシュテーブルでも、ハードディスク上でも関係ない。
ミニ図解
コレクション(例: List<T>)
|
v
IEnumerable<T>
|
v
IEnumerator<T>(実際の「回すやつ」)
3. 実践とコードの汎用性
この契約の最大のメリットは「汎用性」だよ。関数やメソッドがIEnumerable<T>を受け取るなら、どんなコレクション型でもOK。配列、リスト、ハッシュセット、キュー、自作コレクションまで全部いける!
例えば、要素の合計を計算する関数を考えてみよう:
// 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 — 今指してる要素を返す(Enumeratorが今どこか)。
- 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と配列、リスト、セット:誰が親戚?
どんな標準コンテナがこのインターフェースを実装してるか見てみよう:
| コレクションの型 | IEnumerable<T>を実装してる? | foreachで回せる? |
|---|---|---|
配列() |
✅ | ✅ |
|
✅ | ✅ |
|
✅(ペア、キー、値で) | ✅ |
|
✅ | ✅ |
|
✅ | ✅ |
|
✅ | ✅ |
string(string)も普通のIEnumerableを実装してるから、文字ごとに回すこともできるよ。
7. 自作Enumerableの実装
面白い課題:0からNまでの偶数だけを持つ最小限のコレクションを作って、IEnumerable<int>を実装してみよう。これでループも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);
}
GO TO FULL VERSION