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();
}

たった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で回せる?
配列(
int[]
List<T>
Dictionary<TKey, V>
✅(ペア、キー、値で)
HashSet<T>
Queue<T>
Stack<T>

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]とか)、AddRemoveみたいなメソッドを使いたい場合、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