C# IEnumerableとは?Listとの違い・使いどころ・遅延評価を初心者向けに徹底解説

はじめに

C#を学び始めると、IEnumerableという言葉をよく見かけます。

たとえば、次のようなコードです。

C#
IEnumerable<string> names = new List<string>
{
"Alice",
"Bob",
"Charlie"
};

foreach (var name in names)
{
Console.WriteLine(name);
}

Listは見たことがあっても、IEnumerableになると急に難しく感じる人は多いです。

しかし、IEnumerableはC#でコレクションを扱ううえで非常に重要な考え方です。LINQ、foreachyield return、遅延評価、メソッド設計など、実務でも頻繁に登場します。

この記事では、C#のIEnumerableについて、初心者にもわかりやすく解説します。

Listとの違い、使いどころ、遅延評価、注意点、実務でのベストプラクティスまで順番に見ていきましょう。

1. C#のIEnumerableとは?初心者向けに一言で解説

C#のIEnumerableを一言で説明すると、「順番に要素を取り出せるもの」を表す仕組みです。

もう少し具体的に言うと、foreachで1つずつ値を取り出せるオブジェクトがIEnumerableです。

C#
IEnumerable<int> numbers = new List<int> { 1, 2, 3 };

foreach (var number in numbers)
{
Console.WriteLine(number);
}

このコードでは、numbersから123を順番に取り出しています。

IEnumerable自体は、要素を追加したり削除したりするためのものではありません。あくまで「順番に取り出せる」という性質を表します。

1-1. IEnumerableは「順番に取り出せるもの」を表すインターフェース

IEnumerableはインターフェースです。

インターフェースとは、「この機能を持っています」というルールのようなものです。

IEnumerableの場合は、「中身を順番に取り出せる」というルールを表します。

たとえば、次のようなものはすべて順番に取り出せます。

C#
List<int> list = new List<int> { 1, 2, 3 };
int[] array = { 1, 2, 3 };
Dictionary<string, int> dictionary = new Dictionary<string, int>
{
{ "A", 1 },
{ "B", 2 }
};

これらは具体的な型は違いますが、どれもforeachで回せます。

C#
foreach (var item in list)
{
Console.WriteLine(item);
}

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

foreach (var item in dictionary)
{
Console.WriteLine($"{item.Key}: {item.Value}");
}

つまり、IEnumerableは「Listかどうか」「配列かどうか」ではなく、「順番に取り出せるかどうか」に注目した型です。

1-2. foreachで回せるコレクションの共通ルール

C#では、foreachで回せる多くのコレクションがIEnumerableを実装しています。

foreachは内部的に、対象のオブジェクトから列挙子を取り出し、順番に要素を取得しています。

C#
foreach (var name in names)
{
Console.WriteLine(name);
}

このように書くと、C#が自動的に「次の要素はあるか」「次の値は何か」を確認しながら処理してくれます。

そのため、開発者は難しい処理を書かなくても、コレクションの中身を簡単に取り出せます。

IEnumerableは、このforeachと非常に関係が深い存在です。

1-3. IEnumerable<T>とIEnumerableの違い

C#には、IEnumerable<T>IEnumerableがあります。

よく使うのはIEnumerable<T>です。

C#
IEnumerable<int> numbers;
IEnumerable<string> names;
IEnumerable<Person> people;

Tには、要素の型が入ります。

たとえば、IEnumerable<int>なら「intを順番に取り出せるもの」、IEnumerable<string>なら「stringを順番に取り出せるもの」です。

一方、型引数のないIEnumerableもあります。

C#
IEnumerable items;

こちらは古い書き方で、要素の型が明確ではありません。取り出した値は基本的にobjectとして扱われます。

C#
IEnumerable items = new ArrayList { 1, "text", true };

foreach (object item in items)
{
Console.WriteLine(item);
}

現在のC#では、特別な理由がない限りIEnumerable<T>を使うのが一般的です。

型が明確になるため、安全で読みやすく、LINQとも相性が良いからです。

1-4. List・配列・DictionaryもIEnumerableとして扱える理由

List、配列、Dictionaryは、それぞれ具体的な型です。

C#
List<int> list = new List<int> { 1, 2, 3 };
int[] array = { 1, 2, 3 };
Dictionary<string, int> scores = new Dictionary<string, int>
{
{ "Alice", 90 },
{ "Bob", 80 }
};

これらはすべてIEnumerableとして扱えます。

C#
IEnumerable<int> numbers1 = list;
IEnumerable<int> numbers2 = array;
IEnumerable<KeyValuePair<string, int>> numbers3 = scores;

理由は、それぞれの型がIEnumerable<T>を実装しているからです。

つまり、List<int>は「Listとしての機能」も持っていますが、それに加えて「intを順番に取り出せる」というIEnumerable<int>としての機能も持っています。

この考え方はとても重要です。

具体的な型に依存せず、「順番に取り出せればよい」という場面では、ListではなくIEnumerableを使うことでコードが柔軟になります。

2. IEnumerableの基本的な使い方

IEnumerableの基本的な使い方は、主に次のような場面です。

foreachで回す、LINQと組み合わせる、メソッドの引数や戻り値に使う、必要に応じてToListToArrayで変換する、という使い方がよくあります。

順番に見ていきましょう。

2-1. foreachで要素を1つずつ取り出す

IEnumerableのもっとも基本的な使い方は、foreachで要素を1つずつ取り出すことです。

C#
IEnumerable<string> fruits = new List<string>
{
"Apple",
"Banana",
"Orange"
};

foreach (var fruit in fruits)
{
Console.WriteLine(fruit);
}

実行結果は次のようになります。

Apple
Banana
Orange

IEnumerable<string>は「stringを順番に取り出せるもの」です。

そのため、foreachの中では1件ずつstringとして扱えます。

2-2. LINQのWhere・Select・OrderByと組み合わせる

IEnumerableはLINQと非常に相性が良いです。

LINQを使うと、条件で絞り込んだり、値を変換したり、並び替えたりできます。

C#
var numbers = new List<int> { 1, 2, 3, 4, 5 };

IEnumerable<int> evenNumbers = numbers.Where(n => n % 2 == 0);

foreach (var number in evenNumbers)
{
Console.WriteLine(number);
}

この例では、Whereを使って偶数だけを取り出しています。

実行結果は次のとおりです。

2
4

Selectを使うと、値を別の形に変換できます。

C#
var names = new List<string> { "Alice", "Bob", "Charlie" };

IEnumerable<int> nameLengths = names.Select(name => name.Length);

foreach (var length in nameLengths)
{
Console.WriteLine(length);
}

OrderByを使うと並び替えができます。

C#
var scores = new List<int> { 80, 100, 60, 90 };

IEnumerable<int> orderedScores = scores.OrderBy(score => score);

foreach (var score in orderedScores)
{
Console.WriteLine(score);
}

LINQの多くのメソッドは、IEnumerable<T>を受け取り、IEnumerable<T>を返します。

そのため、次のように処理をつなげて書けます。

C#
var result = numbers
.Where(n => n >= 2)
.Select(n => n * 10)
.OrderBy(n => n);

このように、IEnumerableはLINQの土台になる重要な型です。

2-3. メソッドの引数にIEnumerableを使う例

メソッドの引数にIEnumerableを使うと、Listでも配列でも渡せる柔軟なメソッドになります。

C#
void PrintNames(IEnumerable<string> names)
{
foreach (var name in names)
{
Console.WriteLine(name);
}
}

このメソッドには、List<string>も渡せます。

C#
var list = new List<string> { "Alice", "Bob" };
PrintNames(list);

配列も渡せます。

C#
string[] array = { "Charlie", "Dave" };
PrintNames(array);

このように、メソッドの中で「順番に取り出すだけ」でよいなら、引数はList<T>ではなくIEnumerable<T>にすると便利です。

C#
void PrintNumbers(IEnumerable<int> numbers)
{
foreach (var number in numbers)
{
Console.WriteLine(number);
}
}

この設計にすると、呼び出し側は具体的なコレクション型を気にせず使えます。

2-4. メソッドの戻り値にIEnumerableを使う例

メソッドの戻り値としてIEnumerableを使うこともよくあります。

C#
IEnumerable<int> GetEvenNumbers(List<int> numbers)
{
return numbers.Where(n => n % 2 == 0);
}

使う側は、返ってきた結果をforeachで回せます。

C#
var numbers = new List<int> { 1, 2, 3, 4, 5 };

foreach (var number in GetEvenNumbers(numbers))
{
Console.WriteLine(number);
}

戻り値をIEnumerable<T>にすると、「結果を順番に取り出せる」ということだけを呼び出し側に伝えられます。

ただし、戻り値をIEnumerable<T>にする場合は、遅延評価に注意が必要です。

C#
IEnumerable<int> GetNumbers()
{
Console.WriteLine("GetNumbers called");

return new List<int> { 1, 2, 3 }
.Where(n =>
{
Console.WriteLine($"Where: {n}");
return n >= 2;
});
}

このようなLINQを返す場合、メソッドを呼んだ時点ではまだWhereの処理が実行されないことがあります。

実際にforeachしたときに処理が走ります。

2-5. ToList・ToArrayで具体的なコレクションに変換する

IEnumerable<T>は、必要に応じてList<T>や配列に変換できます。

C#
IEnumerable<int> numbers = Enumerable.Range(1, 5);

List<int> list = numbers.ToList();
int[] array = numbers.ToArray();

ToList()を使うとList<T>になります。

C#
var list = numbers.ToList();
list.Add(6);

ToArray()を使うと配列になります。

C#
var array = numbers.ToArray();
Console.WriteLine(array[0]);

IEnumerable<T>のままでは、Addやインデックスアクセスができないことがあります。

そのような機能が必要な場合は、ToList()ToArray()で具体的なコレクションに変換します。

ただし、ToList()ToArray()を呼ぶと、その時点で列挙が実行され、メモリ上に結果が作られます。

便利ですが、むやみに使いすぎないようにしましょう。

3. IEnumerableとListの違い

IEnumerableListの違いは、C#初心者が特につまずきやすいポイントです。

簡単に言うと、IEnumerableは「順番に取り出せるもの」、Listは「要素を保持し、追加・削除・インデックスアクセスなどができる具体的なコレクション」です。

3-1. IEnumerableは「列挙できる」、Listは「保持・追加・削除できる」

IEnumerable<T>は、要素を順番に取り出すためのインターフェースです。

C#
IEnumerable<int> numbers = new List<int> { 1, 2, 3 };

この変数numbersからは、foreachで値を取り出せます。

C#
foreach (var number in numbers)
{
Console.WriteLine(number);
}

しかし、IEnumerable<T>として扱っているため、次のような操作はできません。

C#
// numbers.Add(4);     // コンパイルエラー
// numbers.Remove(1); // コンパイルエラー
// numbers[0]; // コンパイルエラー

一方、List<T>は具体的なコレクションなので、追加や削除ができます。

C#
List<int> list = new List<int> { 1, 2, 3 };

list.Add(4);
list.Remove(2);

Console.WriteLine(list[0]);

つまり、同じデータでも、IEnumerable<T>として見るか、List<T>として見るかで使える機能が変わります。

3-2. Add・Remove・Count・インデックスアクセスの違い

List<T>では、次のような操作ができます。

C#
var list = new List<string> { "A", "B", "C" };

list.Add("D");
list.Remove("A");

Console.WriteLine(list.Count);
Console.WriteLine(list[0]);

一方、IEnumerable<T>では、基本的に順番に取り出すことしかできません。

C#
IEnumerable<string> items = list;

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

IEnumerable<T>には、AddRemoveはありません。

Countについても注意が必要です。

List<T>にはプロパティとしてCountがあります。

C#
int count = list.Count;

一方、IEnumerable<T>で件数を取得する場合は、LINQのCount()メソッドを使うことが多いです。

C#
int count = items.Count();

ただし、Count()は場合によっては全要素を列挙するため、処理コストがかかることがあります。

インデックスアクセスも同様です。

List<T>なら次のように書けます。

C#
var first = list[0];

IEnumerable<T>では直接インデックスアクセスできません。

C#
var first = items.First();

または、どうしてもインデックスアクセスが必要ならToList()します。

C#
var itemList = items.ToList();
var firstItem = itemList[0];

3-3. 読み取り中心ならIEnumerable、変更が必要ならList

使い分けの基本はシンプルです。

データを読み取るだけならIEnumerable<T>で十分です。

C#
void PrintItems(IEnumerable<string> items)
{
foreach (var item in items)
{
Console.WriteLine(item);
}
}

このメソッドは、渡されたデータを表示するだけです。追加や削除はしません。

このような場合、引数はList<string>ではなくIEnumerable<string>にすると柔軟です。

一方、メソッド内で要素を追加・削除したい場合は、List<T>を使います。

C#
void AddDefaultItems(List<string> items)
{
items.Add("Default");
}

このメソッドはAddを使うため、IEnumerable<string>ではなくList<string>が必要です。

3-4. メモリ使用量とパフォーマンスの考え方

IEnumerable<T>は、必ずしもすべてのデータをメモリに持っているとは限りません。

たとえば、LINQのWhereSelectは、必要になるまで処理を実行しない遅延評価を行います。

C#
var numbers = Enumerable.Range(1, 1000000);

IEnumerable<int> evenNumbers = numbers.Where(n => n % 2 == 0);

この時点では、偶数だけをすべてメモリに保存しているわけではありません。

実際にforeachで取り出すときに、1つずつ判定されます。

一方、ToList()すると、その時点で結果をすべてリストに格納します。

C#
List<int> evenNumberList = numbers
.Where(n => n % 2 == 0)
.ToList();

この場合、結果がメモリ上に保持されます。

一度だけ順番に処理するならIEnumerable<T>のままが効率的なことがあります。

一方、何度も使う、件数を何度も見る、インデックスでアクセスする、といった場合はList<T>にしたほうが扱いやすく、結果的に効率が良いこともあります。

3-5. IEnumerableとListの使い分け早見表

観点IEnumerable<T>List<T>
役割順番に取り出すデータを保持して操作する
foreachできるできる
Addできないできる
Removeできないできる
CountCount()で取得することが多いCountプロパティで取得
インデックスアクセス基本できないできる
遅延評価あり得る基本的にデータを保持
向いている用途読み取り、LINQ、柔軟な引数追加・削除・更新、複数回利用

読み取りだけならIEnumerable<T>、変更や再利用が必要ならList<T>と考えるとわかりやすいです。

4. IEnumerableの遅延評価とは?

IEnumerableを理解するうえで重要なのが、遅延評価です。

遅延評価とは、「必要になるまで処理を実行しない」仕組みです。

特にLINQを使うとき、この仕組みを知らないと「なぜこのタイミングで処理が動くのか」がわからなくなります。

4-1. 遅延評価は「必要になるまで処理を実行しない」仕組み

次のコードを見てください。

C#
var numbers = new List<int> { 1, 2, 3, 4, 5 };

var query = numbers.Where(n =>
{
Console.WriteLine($"Where: {n}");
return n % 2 == 0;
});

Console.WriteLine("クエリ作成完了");

このコードでは、Whereの中にConsole.WriteLineがあります。

しかし、queryを作っただけではWhereの処理は実行されません。

実行されるのは、実際に値を取り出したときです。

C#
foreach (var number in query)
{
Console.WriteLine($"結果: {number}");
}

このように、IEnumerable<T>は「処理の結果そのもの」ではなく、「どう取り出すかの手順」を表していることがあります。

4-2. LINQクエリが実行されるタイミング

LINQクエリは、次のようなタイミングで実行されます。

C#
foreach (var item in query)
{
Console.WriteLine(item);
}
C#
var list = query.ToList();
C#
var array = query.ToArray();
C#
var count = query.Count();
C#
var first = query.First();

つまり、実際に結果が必要になったタイミングで実行されます。

逆に、次のように書いただけでは実行されないことが多いです。

C#
var query = numbers.Where(n => n > 3);

これは、「3より大きい数を取り出す処理」を作っただけです。

まだ実際の取り出しは行われていません。

4-3. foreachした瞬間に処理が走る例

遅延評価の動きを確認するために、次のコードを見てみましょう。

C#
var numbers = new List<int> { 1, 2, 3 };

var query = numbers.Where(n =>
{
Console.WriteLine($"判定中: {n}");
return n >= 2;
});

Console.WriteLine("foreach前");

foreach (var number in query)
{
Console.WriteLine($"出力: {number}");
}

Console.WriteLine("foreach後");

出力は次のようになります。

foreach前
判定中: 1
判定中: 2
出力: 2
判定中: 3
出力: 3
foreach後

Whereの処理は、queryを作った時点では実行されていません。

foreachで1件ずつ取り出すタイミングで実行されています。

これが遅延評価です。

4-4. ToListで即時実行される例

ToList()を使うと、その時点でLINQクエリが実行されます。

C#
var numbers = new List<int> { 1, 2, 3 };

var list = numbers
.Where(n =>
{
Console.WriteLine($"判定中: {n}");
return n >= 2;
})
.ToList();

Console.WriteLine("ToList後");

この場合、ToList()を呼んだ時点でWhereが実行されます。

出力は次のようになります。

判定中: 1
判定中: 2
判定中: 3
ToList後

ToList()は、IEnumerable<T>の内容をすべて列挙して、List<T>としてメモリに保存します。

そのため、遅延評価を終わらせて結果を固定したいときに使えます。

4-5. 遅延評価のメリット:無駄な処理とメモリ消費を減らせる

遅延評価には大きなメリットがあります。

必要な分だけ処理できるため、無駄な計算やメモリ消費を減らせます。

たとえば、次のように大量の数値から最初の5件だけ取り出す場合を考えます。

C#
var result = Enumerable.Range(1, 1000000)
.Where(n => n % 2 == 0)
.Take(5);

この時点では、100万件すべてを処理しているわけではありません。

実際にforeachすると、必要な分だけ処理されます。

C#
foreach (var number in result)
{
Console.WriteLine(number);
}

Take(5)があるため、条件に合う5件が見つかれば処理を止められます。

もし最初からすべてをToList()してしまうと、大量のデータをメモリに保持することになります。

そのため、大量データを扱う場合、IEnumerable<T>と遅延評価は非常に役立ちます。

5. IEnumerableで初心者がつまずきやすいポイント

IEnumerableは便利ですが、初心者がつまずきやすい点もあります。

特に、遅延評価、複数回列挙、Count()First()による実行タイミング、データベースアクセスとの関係には注意が必要です。

5-1. 同じIEnumerableを何度もforeachすると処理が繰り返される

IEnumerable<T>は、列挙するたびに処理が実行されることがあります。

C#
var numbers = new List<int> { 1, 2, 3 };

var query = numbers.Where(n =>
{
Console.WriteLine($"判定: {n}");
return n >= 2;
});

foreach (var number in query)
{
Console.WriteLine(number);
}

foreach (var number in query)
{
Console.WriteLine(number);
}

この場合、Whereの処理は2回実行されます。

1回目のforeachで実行され、2回目のforeachでも再び実行されます。

結果を固定して何度も使いたい場合は、ToList()しておくとよいです。

C#
var list = query.ToList();

foreach (var number in list)
{
Console.WriteLine(number);
}

foreach (var number in list)
{
Console.WriteLine(number);
}

これなら、Whereの処理はToList()時に1回だけ実行されます。

5-2. Count()・Any()・First()で意図せず列挙が発生する

IEnumerable<T>に対してCount()Any()First()などを呼ぶと、そのタイミングで列挙が発生します。

C#
var query = numbers.Where(n =>
{
Console.WriteLine($"判定: {n}");
return n > 2;
});

var count = query.Count();

このコードでは、Count()を呼んだ時点でWhereが実行されます。

Any()も同様です。

C#
bool exists = query.Any();

ただし、Any()は条件に合う要素が見つかった時点で処理を止められる場合があります。

一方、Count()は件数を数えるために最後まで列挙することが多いです。

存在確認だけなら、Count() > 0よりもAny()を使うのがおすすめです。

C#
if (query.Any())
{
Console.WriteLine("要素があります");
}

次のようなコードは避けたほうがよいことがあります。

C#
if (query.Count() > 0)
{
Console.WriteLine(query.First());
}

この場合、Count()First()で複数回列挙される可能性があります。

必要なら一度ToList()してから使いましょう。

C#
var list = query.ToList();

if (list.Count > 0)
{
Console.WriteLine(list[0]);
}

5-3. データベースアクセスでクエリが複数回実行されるケース

Entity Frameworkなどでデータベースを扱う場合、IEnumerableやLINQの遅延評価には特に注意が必要です。

たとえば、次のようなコードを考えます。

C#
var users = dbContext.Users
.Where(user => user.IsActive);

foreach (var user in users)
{
Console.WriteLine(user.Name);
}

foreach (var user in users)
{
Console.WriteLine(user.Email);
}

このようなケースでは、同じクエリが複数回実行される可能性があります。

データベースアクセスはメモリ上のリスト操作よりも重い処理です。

そのため、同じ結果を複数回使うなら、早めにToList()で取得しておくことがあります。

C#
var users = dbContext.Users
.Where(user => user.IsActive)
.ToList();

foreach (var user in users)
{
Console.WriteLine(user.Name);
}

foreach (var user in users)
{
Console.WriteLine(user.Email);
}

この場合、データベースへの問い合わせはToList()のタイミングで行われ、結果はメモリ上のListとして扱われます。

ただし、ToList()を早く呼びすぎると、不要なデータまでメモリに読み込むことがあります。

データベースでは、できるだけWhereSelectで必要な条件・項目に絞ってからToList()するのが基本です。

5-4. foreach中に元のListを変更してエラーになるケース

IEnumerable<T>としてList<T>foreachしている最中に、元のList<T>を変更するとエラーになることがあります。

C#
var numbers = new List<int> { 1, 2, 3 };

foreach (var number in numbers)
{
if (number == 2)
{
numbers.Remove(number);
}
}

このコードは、実行時に例外が発生する可能性があります。

foreachで列挙している最中にコレクションの内容を変更してはいけません。

削除したい場合は、別の方法を使います。

たとえば、RemoveAllを使います。

C#
numbers.RemoveAll(n => n == 2);

または、削除対象を別リストにしてから処理します。

C#
var targets = numbers.Where(n => n == 2).ToList();

foreach (var target in targets)
{
numbers.Remove(target);
}

ToList()で一度結果を固定しておくことで、元のリストを安全に変更できます。

5-5. varで型が隠れてIEnumerableだと気づきにくい問題

C#ではvarを使うことが多いです。

C#
var result = numbers.Where(n => n > 2);

このコードのresultは、見た目だけでは型がわかりにくいです。

実際には、IEnumerable<int>のような型として扱われます。

そのため、次のように書いてもAddはできません。

C#
// result.Add(10); // コンパイルエラー

Whereの結果はList<int>ではありません。

必要であれば、ToList()します。

C#
var result = numbers
.Where(n => n > 2)
.ToList();

result.Add(10);

varは便利ですが、LINQと組み合わせると型が見えにくくなります。

初心者のうちは、学習目的であえて型を明示してみるのもおすすめです。

C#
IEnumerable<int> result = numbers.Where(n => n > 2);

こう書くと、「これはListではなくIEnumerableなんだ」と理解しやすくなります。

6. IEnumerableとyield returnの関係

yield returnは、IEnumerable<T>を簡単に作るための構文です。

普通にList<T>を作って返す方法もありますが、yield returnを使うと、必要な値を1つずつ返せます。

6-1. yield returnはIEnumerableを簡単に作る構文

通常、複数の値を返したい場合は、リストを作って返します。

C#
IEnumerable<int> GetNumbers()
{
var numbers = new List<int>();

numbers.Add(1);
numbers.Add(2);
numbers.Add(3);

return numbers;
}

これをyield returnで書くと、次のようになります。

C#
IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
yield return 3;
}

とてもシンプルです。

yield returnを使うと、C#が自動的にIEnumerable<T>として列挙できる仕組みを作ってくれます。

6-2. yield returnで1件ずつ値を返すサンプル

次の例では、1から指定した数までを順番に返します。

C#
IEnumerable<int> CountUpTo(int max)
{
for (int i = 1; i <= max; i++)
{
yield return i;
}
}

使う側は普通のIEnumerable<int>として扱えます。

C#
foreach (var number in CountUpTo(5))
{
Console.WriteLine(number);
}

実行結果は次のようになります。

1
2
3
4
5

yield returnの特徴は、すべての値を一度に作るのではなく、必要になったタイミングで1つずつ返せることです。

6-3. returnとの違い

returnは、メソッドの処理を終了して値を返します。

C#
int GetNumber()
{
return 1;
}

一方、yield returnは、値を1つ返してもメソッド全体が完全に終了するわけではありません。

次に列挙されたとき、前回の続きから処理が再開されます。

C#
IEnumerable<int> GetNumbers()
{
Console.WriteLine("1を返す");
yield return 1;

Console.WriteLine("2を返す");
yield return 2;

Console.WriteLine("3を返す");
yield return 3;
}

このメソッドは、foreachで1件ずつ取り出されるたびに進みます。

C#
foreach (var number in GetNumbers())
{
Console.WriteLine($"受け取った値: {number}");
}

yield returnは、複数の値を順番に返すための特別なreturnのようなものだと考えると理解しやすいです。

6-4. 大量データ処理でyield returnが役立つ場面

yield returnは、大量データを扱う場面で役立ちます。

たとえば、大量のログ行を読み込み、条件に合う行だけ返す処理を考えます。

C#
IEnumerable<string> ReadErrorLines(string filePath)
{
foreach (var line in File.ReadLines(filePath))
{
if (line.Contains("ERROR"))
{
yield return line;
}
}
}

このコードでは、エラー行を1件ずつ返します。

すべての行を一度にリストへ読み込む必要がないため、メモリ使用量を抑えられます。

使う側は次のように書けます。

C#
foreach (var errorLine in ReadErrorLines("app.log"))
{
Console.WriteLine(errorLine);
}

大量データを「少しずつ処理したい」場合、IEnumerable<T>yield returnは非常に相性が良いです。

6-5. yield returnを使うときの注意点

yield returnにも注意点があります。

まず、処理は遅延実行されます。

C#
var numbers = GetNumbers();

この時点では、まだGetNumbers()の中身がすべて実行されたわけではありません。

実際にforeachしたときに実行されます。

C#
foreach (var number in numbers)
{
Console.WriteLine(number);
}

また、例外が発生するタイミングにも注意が必要です。

C#
IEnumerable<int> GetNumbers()
{
Console.WriteLine("開始");
yield return 1;
throw new Exception("エラー");
}

このメソッドを呼んだだけでは例外が出ない場合があります。

C#
var numbers = GetNumbers();

実際に列挙したときに例外が発生します。

C#
foreach (var number in numbers)
{
Console.WriteLine(number);
}

さらに、同じIEnumerable<T>を複数回列挙すると、yield returnの処理も複数回実行されます。

結果を固定したい場合は、ToList()を使いましょう。

C#
var list = GetNumbers().ToList();

7. IEnumerableの使いどころ

IEnumerableは、C#の実務コードでよく使われます。

特に、読み取り専用でデータを受け取るとき、具体的な型に依存したくないとき、LINQを使うとき、大量データを扱うときに便利です。

7-1. 読み取り専用のデータを受け取るとき

メソッドの中でデータを読み取るだけなら、引数はIEnumerable<T>にするのが自然です。

C#
void PrintProducts(IEnumerable<string> products)
{
foreach (var product in products)
{
Console.WriteLine(product);
}
}

このメソッドは、商品名を表示するだけです。

要素を追加したり削除したりしません。

そのため、List<string>に限定する必要はありません。

C#
var list = new List<string> { "Pen", "Notebook" };
string[] array = { "Book", "Desk" };

PrintProducts(list);
PrintProducts(array);

読み取りだけなら、IEnumerable<T>を使うことで呼び出し側の自由度が上がります。

7-2. コレクションの具体的な型に依存したくないとき

IEnumerable<T>を使うと、メソッドが特定のコレクション型に依存しにくくなります。

C#
decimal CalculateTotal(IEnumerable<decimal> prices)
{
decimal total = 0;

foreach (var price in prices)
{
total += price;
}

return total;
}

このメソッドは、価格を順番に取り出せれば十分です。

そのため、List<decimal>でも配列でもLINQの結果でも受け取れます。

C#
var list = new List<decimal> { 100, 200, 300 };
var array = new decimal[] { 400, 500 };
var query = list.Where(price => price >= 200);

Console.WriteLine(CalculateTotal(list));
Console.WriteLine(CalculateTotal(array));
Console.WriteLine(CalculateTotal(query));

具体的な型に依存しない設計にすると、コードの再利用性が高まります。

7-3. LINQで条件抽出や変換をつなげたいとき

IEnumerable<T>はLINQと組み合わせることで、読みやすいデータ処理を書けます。

C#
var users = new List<User>
{
new User { Name = "Alice", Age = 25 },
new User { Name = "Bob", Age = 17 },
new User { Name = "Charlie", Age = 30 }
};

var adultNames = users
.Where(user => user.Age >= 20)
.Select(user => user.Name);

このコードでは、20歳以上のユーザーを抽出し、その名前だけを取り出しています。

C#
foreach (var name in adultNames)
{
Console.WriteLine(name);
}

LINQを使うと、条件抽出、変換、並び替えなどを自然につなげられます。

C#
var result = users
.Where(user => user.Age >= 20)
.OrderBy(user => user.Name)
.Select(user => user.Name);

このような処理は、IEnumerable<T>の得意分野です。

7-4. 大量データを少しずつ処理したいとき

大量データを扱うとき、すべてを一度にList<T>へ入れるとメモリを多く使うことがあります。

IEnumerable<T>を使えば、必要な分だけ順番に処理できます。

C#
IEnumerable<int> GetLargeNumbers()
{
for (int i = 1; i <= 1000000; i++)
{
yield return i;
}
}

このメソッドは、100万件の数値を一度にリストとして作るのではなく、必要に応じて1件ずつ返します。

C#
foreach (var number in GetLargeNumbers().Take(10))
{
Console.WriteLine(number);
}

Take(10)を使えば、最初の10件だけ処理できます。

大量データを少しずつ処理したい場合、IEnumerable<T>は非常に有効です。

7-5. APIやメソッド設計で柔軟性を持たせたいとき

メソッドやAPIを設計するとき、引数にIEnumerable<T>を使うと柔軟性が高まります。

C#
void SendEmails(IEnumerable<string> emailAddresses)
{
foreach (var email in emailAddresses)
{
Console.WriteLine($"Send to: {email}");
}
}

このメソッドは、メールアドレスを順番に処理するだけです。

呼び出し側は、List<string>でも配列でもLINQの結果でも渡せます。

C#
SendEmails(new List<string> { "a@example.com", "b@example.com" });

SendEmails(new[]
{
"c@example.com",
"d@example.com"
});

メソッドが必要としている機能が「順番に取り出せること」だけなら、IEnumerable<T>を使うと余計な制約を減らせます。

8. IEnumerableではなくListを使うべき場面

IEnumerable<T>は便利ですが、常に最適というわけではありません。

要素を変更したい場合、件数やインデックスを頻繁に使う場合、複数回ループすることが確定している場合は、List<T>のほうが向いています。

8-1. 要素を追加・削除・更新したいとき

要素を追加・削除したい場合は、List<T>を使います。

C#
var names = new List<string>();

names.Add("Alice");
names.Add("Bob");

names.Remove("Alice");

IEnumerable<T>にはAddRemoveがありません。

C#
IEnumerable<string> names = new List<string> { "Alice", "Bob" };

// names.Add("Charlie"); // コンパイルエラー

また、インデックスを使って値を更新したい場合もList<T>が必要です。

C#
var list = new List<string> { "A", "B", "C" };

list[0] = "Z";

データを変更する前提なら、最初からList<T>を使うほうが自然です。

8-2. Countを何度も使いたいとき

件数を何度も使う場合は、List<T>にしておくと便利です。

C#
var list = numbers.ToList();

Console.WriteLine(list.Count);

if (list.Count > 10)
{
Console.WriteLine("10件より多いです");
}

List<T>Countはプロパティなので、簡単に件数を取得できます。

一方、IEnumerable<T>Count()を使うと、場合によっては毎回列挙が発生します。

C#
var count1 = query.Count();
var count2 = query.Count();

このように何度もCount()を呼ぶと、同じ処理が繰り返される可能性があります。

件数を頻繁に使うなら、ToList()で固定することを検討しましょう。

C#
var list = query.ToList();
var count = list.Count;

8-3. インデックスで高速にアクセスしたいとき

List<T>はインデックスでアクセスできます。

C#
var names = new List<string> { "Alice", "Bob", "Charlie" };

Console.WriteLine(names[0]);
Console.WriteLine(names[1]);

一方、IEnumerable<T>は基本的に先頭から順番に取り出すためのものです。

C#
IEnumerable<string> names = new List<string> { "Alice", "Bob", "Charlie" };

var first = names.First();

LINQのElementAt()を使えば指定位置の要素を取得できます。

C#
var second = names.ElementAt(1);

ただし、元の型によっては先頭から順番にたどる必要があり、効率が悪くなることがあります。

インデックスで頻繁にアクセスするなら、List<T>や配列を使うほうが適しています。

8-4. 複数回ループすることが確定しているとき

同じデータを複数回ループすることが確定している場合は、List<T>にしておくと安心です。

C#
var list = query.ToList();

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

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

IEnumerable<T>のまま複数回ループすると、そのたびに処理が再実行されることがあります。

C#
foreach (var item in query)
{
Console.WriteLine(item);
}

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

この違いは、データベースアクセスや重い計算を含む場合に大きな問題になります。

複数回使うなら、ToList()で結果を固定しましょう。

8-5. 結果を固定して再利用したいとき

LINQの結果をその時点で固定したい場合は、ToList()を使います。

C#
var filteredUsers = users
.Where(user => user.IsActive)
.ToList();

これにより、filteredUsersはその時点の結果を持つList<T>になります。

元のリストがあとから変更されても、filteredUsersは基本的に作成時点の結果を保持します。

C#
var numbers = new List<int> { 1, 2, 3 };

var query = numbers.Where(n => n >= 2);
var list = query.ToList();

numbers.Add(4);

Console.WriteLine("query:");
foreach (var number in query)
{
Console.WriteLine(number);
}

Console.WriteLine("list:");
foreach (var number in list)
{
Console.WriteLine(number);
}

queryは遅延評価なので、あとから追加された4も含まれる可能性があります。

一方、listToList()した時点の結果です。

結果を固定して再利用したいなら、List<T>に変換するのがわかりやすいです。

9. IEnumerable・ICollection・IList・Listの違い

C#には、IEnumerable<T>以外にもICollection<T>IList<T>List<T>があります。

これらは似ていますが、持っている機能が少しずつ違います。

ざっくり言うと、次のような関係です。

IEnumerable<T>

ICollection<T>

IList<T>

List<T>

下に行くほど機能が増えます。

9-1. IEnumerable:順番に取り出せる

IEnumerable<T>は、順番に取り出せることを表します。

C#
IEnumerable<string> items = new List<string> { "A", "B", "C" };

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

できることは基本的に列挙です。

読み取り中心の処理に向いています。

C#
void PrintItems(IEnumerable<string> items)
{
foreach (var item in items)
{
Console.WriteLine(item);
}
}

「とにかく順番に取り出せればよい」という場合に使います。

9-2. ICollection:件数取得や追加・削除の機能を持つ

ICollection<T>は、IEnumerable<T>より多くの機能を持ちます。

主に、件数取得、追加、削除などができます。

C#
ICollection<string> items = new List<string>();

items.Add("A");
items.Add("B");

Console.WriteLine(items.Count);

items.Remove("A");

ICollection<T>にはCountプロパティがあります。

そのため、件数を取得したい場合に便利です。

ただし、インデックスアクセスはできません。

C#
// items[0]; // コンパイルエラー

要素数や追加・削除が必要だが、インデックスアクセスまでは不要という場合に使えます。

9-3. IList:インデックスアクセスができる

IList<T>は、ICollection<T>よりさらに多くの機能を持ちます。

大きな特徴は、インデックスアクセスができることです。

C#
IList<string> items = new List<string> { "A", "B", "C" };

Console.WriteLine(items[0]);

items[1] = "Z";
items.Add("D");
items.Remove("A");

IList<T>を使うと、List<T>のように位置を指定して要素を取得・更新できます。

ただし、IList<T>はインターフェースであり、具体的な実装ではありません。

実際の中身はList<T>などです。

C#
IList<int> numbers = new List<int> { 1, 2, 3 };

インデックスアクセスが必要だが、具体的な実装をList<T>に限定したくない場合に使えます。

9-4. List:IListを実装した具体的なクラス

List<T>は、実際にデータを保持する具体的なクラスです。

C#
List<string> items = new List<string>();

items.Add("A");
items.Add("B");
items.Add("C");

Console.WriteLine(items[0]);
Console.WriteLine(items.Count);

List<T>は、IEnumerable<T>ICollection<T>IList<T>などを実装しています。

そのため、次のように扱えます。

C#
List<string> list = new List<string> { "A", "B" };

IEnumerable<string> enumerable = list;
ICollection<string> collection = list;
IList<string> ilist = list;

同じList<T>でも、どの型として扱うかによって使える機能が変わります。

C#
enumerable; // foreach中心
collection; // Count, Add, Removeなど
ilist; // インデックスアクセスなど
list; // List固有の機能も使える

9-5. 迷ったときの選び方

迷ったときは、メソッドが本当に必要としている機能に合わせて型を選びます。

順番に読み取るだけならIEnumerable<T>です。

C#
void PrintItems(IEnumerable<string> items)
{
foreach (var item in items)
{
Console.WriteLine(item);
}
}

件数や追加・削除が必要ならICollection<T>です。

C#
void AddItem(ICollection<string> items, string item)
{
items.Add(item);
}

インデックスアクセスが必要ならIList<T>です。

C#
void ReplaceFirst(IList<string> items)
{
if (items.Count > 0)
{
items[0] = "Replaced";
}
}

具体的にList<T>の機能を使いたいならList<T>です。

C#
void SortItems(List<string> items)
{
items.Sort();
}

基本的には、必要以上に具体的な型にしないことが大切です。

10. 実務で使えるIEnumerableのベストプラクティス

実務でIEnumerable<T>を使うときは、柔軟性と安全性のバランスが重要です。

何でもIEnumerable<T>にすればよいわけではありません。

ここでは、実務で役立つ考え方を紹介します。

10-1. 引数はIEnumerable、内部で必要ならToListする

メソッドの引数は、読み取りだけならIEnumerable<T>にすると柔軟です。

C#
void ProcessOrders(IEnumerable<Order> orders)
{
foreach (var order in orders)
{
Console.WriteLine(order.Id);
}
}

このメソッドは、List<Order>でも配列でもLINQの結果でも受け取れます。

ただし、内部で複数回ループする場合は、先にToList()しておくと安全です。

C#
void ProcessOrders(IEnumerable<Order> orders)
{
var orderList = orders.ToList();

Console.WriteLine($"件数: {orderList.Count}");

foreach (var order in orderList)
{
Console.WriteLine(order.Id);
}

foreach (var order in orderList)
{
Console.WriteLine(order.TotalAmount);
}
}

このように、外部には柔軟なIEnumerable<T>を受け入れ、内部で必要ならList<T>に変換する設計は実務でよく使います。

10-2. 戻り値は目的に応じてIEnumerableかListを選ぶ

戻り値をIEnumerable<T>にするかList<T>にするかは、目的によって選びます。

処理を遅延評価したい、または順番に取り出せればよいならIEnumerable<T>が向いています。

C#
IEnumerable<User> GetActiveUsers(IEnumerable<User> users)
{
return users.Where(user => user.IsActive);
}

一方、結果を固定して返したい場合はList<T>が向いています。

C#
List<User> GetActiveUserList(IEnumerable<User> users)
{
return users
.Where(user => user.IsActive)
.ToList();
}

呼び出し側に「これはすでに確定したリストです」と伝えたいならList<T>を返すのも自然です。

反対に、「順番に取り出せればよい」「具体的な型を隠したい」という場合はIEnumerable<T>が適しています。

10-3. 複数回列挙するならToListで固定する

同じIEnumerable<T>を複数回使うなら、ToList()で固定しましょう。

C#
void Analyze(IEnumerable<int> numbers)
{
var numberList = numbers.ToList();

var count = numberList.Count;
var max = numberList.Max();
var min = numberList.Min();

foreach (var number in numberList)
{
Console.WriteLine(number);
}
}

このようにすると、元のIEnumerable<T>が重い処理やデータベースクエリだった場合でも、複数回実行されるのを避けられます。

ただし、データ量が非常に大きい場合は、ToList()でメモリを多く使う可能性があります。

複数回列挙によるコストと、ToList()によるメモリ使用量のバランスを考えることが大切です。

10-4. LINQの遅延評価を意識してデバッグする

LINQを使っていると、「この行で実行されていると思ったのに、実際は別のタイミングだった」ということがあります。

C#
var query = users.Where(user =>
{
Console.WriteLine(user.Name);
return user.IsActive;
});

この時点では、まだConsole.WriteLineが実行されないことがあります。

実行されるのは、次のように列挙したときです。

C#
var list = query.ToList();

または、

C#
foreach (var user in query)
{
Console.WriteLine(user.Name);
}

デバッグ時には、IEnumerable<T>の中身を確認するためにウォッチ式やToList()を使うことがあります。

ただし、デバッグ目的でToList()すると、その時点でクエリが実行されます。

データベースアクセスを含む場合は、意図せずSQLが実行されることもあるため注意しましょう。

10-5. パフォーマンス問題を避けるチェックポイント

IEnumerable<T>を使うときは、次の点を意識するとパフォーマンス問題を避けやすくなります。

チェックポイント確認すること
複数回列挙していないか同じIEnumerable<T>を何度もforeachしていないか
Count()を多用していないか件数取得で毎回列挙していないか
ToList()が早すぎないか不要なデータまでメモリに読み込んでいないか
データベースクエリを意識しているかToList()前に条件を絞っているか
遅延評価のタイミングを理解しているかいつ実行されるか把握しているか

特に注意したいのは、データベースアクセスと複数回列挙です。

C#
var users = dbContext.Users.Where(user => user.IsActive);

var count = users.Count();

foreach (var user in users)
{
Console.WriteLine(user.Name);
}

このコードでは、Count()foreachでクエリが複数回実行される可能性があります。

必要であれば、次のように書きます。

C#
var users = dbContext.Users
.Where(user => user.IsActive)
.ToList();

var count = users.Count;

foreach (var user in users)
{
Console.WriteLine(user.Name);
}

ただし、大量データを一気に取得しないように、WhereSelectで必要なデータに絞ってからToList()するのが基本です。

11. IEnumerableに関するよくある質問

ここでは、C#のIEnumerableについて初心者が疑問に思いやすいポイントをQ&A形式で解説します。

11-1. IEnumerableは読み取り専用ですか?

IEnumerable<T>自体は、要素を順番に取り出すためのインターフェースです。

そのため、IEnumerable<T>の変数からはAddRemoveはできません。

C#
IEnumerable<int> numbers = new List<int> { 1, 2, 3 };

// numbers.Add(4); // コンパイルエラー

この意味では、IEnumerable<T>は読み取り専用のように見えます。

ただし、元の実体がList<T>であれば、そのList<T>を通じて変更できます。

C#
var list = new List<int> { 1, 2, 3 };

IEnumerable<int> numbers = list;

list.Add(4);

foreach (var number in numbers)
{
Console.WriteLine(number);
}

この場合、numbersを列挙すると追加後の4も出てくる可能性があります。

つまり、IEnumerable<T>は「変更できないコレクション」を保証するものではありません。

あくまで「IEnumerableとして扱っている間は、列挙しかできない」と考えましょう。

11-2. IEnumerableにAddできないのはなぜですか?

IEnumerable<T>は、「順番に取り出せる」という機能だけを表すインターフェースだからです。

Addできるかどうかは、IEnumerable<T>の役割ではありません。

C#
IEnumerable<string> items = new List<string> { "A", "B" };

// items.Add("C"); // コンパイルエラー

Addしたい場合は、List<T>ICollection<T>として扱う必要があります。

C#
List<string> list = new List<string> { "A", "B" };
list.Add("C");

または、

C#
ICollection<string> collection = new List<string> { "A", "B" };
collection.Add("C");

IEnumerable<T>は、あえて機能を少なくすることで、さまざまなコレクションを共通して扱えるようにしています。

11-3. IEnumerableと配列は何が違いますか?

配列は具体的なデータ構造です。

C#
int[] array = { 1, 2, 3 };

配列は要素数が固定で、インデックスアクセスができます。

C#
Console.WriteLine(array[0]);

array[1] = 10;

一方、IEnumerable<T>はインターフェースです。

C#
IEnumerable<int> numbers = array;

IEnumerable<T>として扱うと、基本的には順番に取り出すことが中心になります。

C#
foreach (var number in numbers)
{
Console.WriteLine(number);
}

配列はIEnumerable<T>として扱えますが、IEnumerable<T>が必ず配列であるとは限りません。

List<T>かもしれませんし、LINQの結果かもしれませんし、yield returnで生成されたものかもしれません。

つまり、配列は具体的な入れ物、IEnumerable<T>は「順番に取り出せる」という共通ルールです。

11-4. ToListはいつ使うべきですか?

ToList()は、IEnumerable<T>の結果をその時点で固定したいときに使います。

たとえば、次のような場面です。

C#
var list = query.ToList();

複数回ループする場合。

C#
var list = query.ToList();

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

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

件数を何度も使う場合。

C#
var list = query.ToList();

if (list.Count > 0)
{
Console.WriteLine(list.Count);
}

インデックスアクセスしたい場合。

C#
var list = query.ToList();

Console.WriteLine(list[0]);

データベースクエリを実行して結果をメモリに取得したい場合。

C#
var users = dbContext.Users
.Where(user => user.IsActive)
.ToList();

ただし、ToList()を使うと結果をすべてメモリに読み込みます。

大量データではメモリ使用量に注意しましょう。

11-5. IEnumerableはパフォーマンスが悪いですか?

IEnumerable<T>自体がパフォーマンスの悪いものではありません。

むしろ、遅延評価によって無駄な処理やメモリ使用量を減らせることがあります。

C#
var result = Enumerable.Range(1, 1000000)
.Where(n => n % 2 == 0)
.Take(10);

このような処理では、必要な分だけ取り出せるため効率的です。

ただし、使い方によってはパフォーマンスが悪くなることがあります。

たとえば、同じIEnumerable<T>を何度も列挙する場合です。

C#
var count = query.Count();

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

この場合、処理が複数回実行される可能性があります。

また、Count()を何度も呼ぶと、毎回列挙されることがあります。

C#
if (query.Count() > 0)
{
Console.WriteLine(query.Count());
}

このような場合は、ToList()してから使うほうがよいことがあります。

C#
var list = query.ToList();

if (list.Count > 0)
{
Console.WriteLine(list.Count);
}

結論として、IEnumerable<T>が遅いのではなく、遅延評価や列挙タイミングを意識せずに使うと遅くなることがある、という理解が正確です。

まとめ

C#のIEnumerableは、「順番に要素を取り出せるもの」を表すインターフェースです。

List、配列、Dictionaryなど、多くのコレクションはIEnumerable<T>として扱えます。

IEnumerable<T>を使うと、具体的なコレクション型に依存せず、柔軟なコードを書けます。

特に、foreachで順番に処理したい場合や、LINQで条件抽出・変換・並び替えを行いたい場合に便利です。

一方で、IEnumerable<T>にはAddRemove、インデックスアクセスなどの機能はありません。

要素を追加・削除・更新したい場合や、件数を何度も取得したい場合、複数回ループすることが確定している場合は、List<T>を使うほうが適しています。

また、IEnumerable<T>では遅延評価が重要です。

LINQのWhereSelectは、書いた瞬間に実行されるのではなく、foreachToList()Count()First()などで実際に値が必要になったタイミングで実行されます。

この仕組みを理解していないと、同じ処理が何度も実行されたり、データベースクエリが複数回走ったりする原因になります。

実務では、次の考え方を覚えておくと便利です。

読み取りだけならIEnumerable<T>、変更が必要ならList<T>

引数は柔軟にIEnumerable<T>で受け取り、内部で複数回使うならToList()で固定する。

戻り値は、遅延評価したいならIEnumerable<T>、結果を固定して返したいならList<T>

IEnumerable<T>はC#の基本であり、LINQやコレクション操作を理解するための重要な土台です。

最初は少し難しく感じるかもしれませんが、「順番に取り出せるもの」「必要になるまで実行されないことがある」という2つのポイントを押さえるだけで、かなり理解しやすくなります。