C#のIEnumerableとは?Listとの違い・LINQ・遅延実行まで初心者向けに徹底解説
はじめに
C#で配列やListを扱っていると、IEnumerableやIEnumerable<T>という型をよく見かけます。
たとえば、次のようなコードです。
C#IEnumerable<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
foreach (var number in numbers)
{
Console.WriteLine(number);
}
初心者のうちは「Listと何が違うの?」「なぜAddできないの?」「LINQでよく出てくるけど何者?」と感じやすいポイントです。
IEnumerableは、C#のコレクション処理を理解するうえで非常に重要な仕組みです。List、配列、Dictionary、LINQ、foreach、遅延実行、yield returnなど、多くの機能と深く関係しています。
この記事では、c# enumerableを理解したい初心者向けに、IEnumerableの基本、Listとの違い、LINQとの関係、遅延実行、yield return、実務での使い分けまで順番に解説します。
1. C#のIEnumerableとは?まず押さえるべき基本
1-1. IEnumerableは「順番に取り出せる」ことを表すインターフェース
IEnumerableとは、簡単にいうと「要素を順番に取り出せるもの」を表すインターフェースです。
インターフェースとは、「この機能を持っています」という約束のようなものです。IEnumerableを実装している型は、外部から順番に要素を取り出せます。
たとえば、Listは複数の値を持つコレクションです。
C#var names = new List<string> { "Alice", "Bob", "Charlie" };
このListは、先頭から順番に要素を取り出せます。
C#foreach (var name in names)
{
Console.WriteLine(name);
}
このように、foreachで順番に処理できるものの多くは、IEnumerableとして扱えます。
IEnumerableの本質は、「データを持っていること」ではなく、「順番に列挙できること」です。
1-2. IEnumerable<T>と非ジェネリックIEnumerableの違い
C#には、主に次の2種類のIEnumerableがあります。
C#IEnumerable
IEnumerable<T>
現在のC#では、基本的にIEnumerable<T>を使うことが多いです。
IEnumerable<T>のTは、要素の型を表します。
C#IEnumerable<int> numbers;
IEnumerable<string> names;
IEnumerable<Person> people;
IEnumerable<int>であれば「int型の値を順番に取り出せるもの」、IEnumerable<string>であれば「string型の値を順番に取り出せるもの」という意味になります。
一方、非ジェネリックのIEnumerableは、要素をobjectとして扱います。
C#IEnumerable items = new ArrayList();
非ジェネリックのIEnumerableでは、取り出した値の型が明確ではないため、キャストが必要になることがあります。
C#foreach (object item in items)
{
Console.WriteLine(item);
}
型安全性や使いやすさを考えると、通常はIEnumerable<T>を使うのがおすすめです。
1-3. 配列・List・DictionaryなどもIEnumerableとして扱える
IEnumerableは特別なコレクション専用の型ではありません。C#の多くのコレクションはIEnumerableとして扱えます。
代表的な例は次のとおりです。
C#int[] array = { 1, 2, 3 };
List<int> list = new List<int> { 1, 2, 3 };
Dictionary<string, int> dictionary = new Dictionary<string, int>
{
{ "Apple", 100 },
{ "Banana", 200 }
};
これらはすべてforeachで処理できます。
C#foreach (var number in array)
{
Console.WriteLine(number);
}
foreach (var number in list)
{
Console.WriteLine(number);
}
foreach (var item in dictionary)
{
Console.WriteLine($"{item.Key}: {item.Value}");
}
配列もListもDictionaryも、内部構造は異なります。しかし、「順番に取り出せる」という共通点があります。
その共通点を表すのがIEnumerableです。
1-4. foreachでIEnumerableを使える理由
foreachは、IEnumerableを使って要素を順番に取り出します。
たとえば、次のコードがあります。
C#IEnumerable<string> names = new List<string> { "Alice", "Bob", "Charlie" };
foreach (var name in names)
{
Console.WriteLine(name);
}
foreachは、内部的には列挙子を取得して、次の要素があるかを確認しながら処理を進めます。
イメージとしては、次のような動きです。
C#var enumerator = names.GetEnumerator();
while (enumerator.MoveNext())
{
var name = enumerator.Current;
Console.WriteLine(name);
}
実際には例外処理や後片付けなども関係しますが、基本的な考え方はこのような流れです。
IEnumerableは「列挙子を取得できる」ため、foreachで使えます。
1-5. IEnumerableが初心者にわかりにくい理由
IEnumerableが初心者にとってわかりにくい理由は、Listのように実体が見えにくいからです。
Listはわかりやすいです。
C#var list = new List<int>();
list.Add(1);
list.Add(2);
list.Add(3);
値を追加でき、削除でき、インデックスでアクセスできます。
一方、IEnumerableは次のように使います。
C#IEnumerable<int> numbers = list;
このとき、numbersに対してはAddやRemoveは使えません。
C#// numbers.Add(4); // コンパイルエラー
そのため、「中身はListなのになぜAddできないの?」と混乱しやすくなります。
ポイントは、変数の型がIEnumerable<int>である場合、その変数から見える機能はIEnumerable<int>に定義されている機能だけになるということです。
IEnumerableは「順番に取り出す」ための型であり、「追加・削除・更新する」ための型ではありません。
2. IEnumerableとListの違い
2-1. IEnumerableとListの役割の違い
IEnumerableとListは、どちらも複数の要素を扱う場面で使われます。しかし、役割は大きく異なります。
IEnumerable<T>は、要素を順番に取り出すための抽象的な型です。
C#IEnumerable<int> numbers;
一方、List<T>は、要素を内部に保持し、追加・削除・検索・インデックスアクセスなどができる具体的なコレクションです。
C#List<int> numbers = new List<int>();
つまり、IEnumerableは「列挙できるもの」という性質を表し、Listは「実際にデータを格納して操作できるコレクション」です。
2-2. IEnumerableは読み取り・列挙向き、Listは追加・削除・更新向き
IEnumerableは、基本的に要素を順番に読み取る用途に向いています。
C#IEnumerable<string> names = new List<string> { "Alice", "Bob", "Charlie" };
foreach (var name in names)
{
Console.WriteLine(name);
}
このように、データを順番に処理するだけならIEnumerableで十分です。
一方、要素を追加したり削除したりする場合は、Listを使います。
C#List<string> names = new List<string>();
names.Add("Alice");
names.Add("Bob");
names.Remove("Alice");
更新やインデックスアクセスもListの得意分野です。
C#names[0] = "Charlie";
Console.WriteLine(names[0]);
IEnumerableは「読み取る側」、Listは「管理・変更する側」と考えると理解しやすくなります。
2-3. メモリ使用量とパフォーマンスの違い
Listは、要素をメモリ上に保持します。そのため、すべてのデータを手元に置いて操作できます。
C#List<int> numbers = Enumerable.Range(1, 1000000).ToList();
この場合、100万件の整数がListとしてメモリに保持されます。
一方、IEnumerableは必ずしもすべての要素をすぐにメモリ上へ持つとは限りません。
C#IEnumerable<int> numbers = Enumerable.Range(1, 1000000)
.Where(x => x % 2 == 0);
このコードでは、Whereによる絞り込み結果がすぐにすべて作られるわけではありません。実際に列挙されたタイミングで、必要に応じて処理されます。
この仕組みを遅延実行と呼びます。
ただし、IEnumerableなら必ず高速・省メモリというわけではありません。同じIEnumerableを何度も列挙すると、そのたびに処理が実行され、かえって遅くなることもあります。
2-4. Count・Add・Removeが使えない理由
IEnumerable<T>には、AddやRemoveといったメソッドは定義されていません。
C#IEnumerable<int> numbers = new List<int> { 1, 2, 3 };
// numbers.Add(4); // 使えない
// numbers.Remove(2); // 使えない
これは、IEnumerableの役割が「順番に取り出すこと」だからです。
同じ理由で、numbers.Countのようなプロパティも使えません。
C#// int count = numbers.Count; // 使えない
ただし、LINQの拡張メソッドであるCount()は使えます。
C#int count = numbers.Count();
ここで注意したいのは、Count()は場合によっては中身を最初から最後まで列挙して数えることがあるという点です。
ListであればCountプロパティで高速に件数を取得できます。
C#List<int> list = new List<int> { 1, 2, 3 };
int count = list.Count;
IEnumerableのCount()は便利ですが、大量データや重い処理を含むLINQ結果では、何度も呼ばないように注意が必要です。
2-5. IEnumerableとListの違いを表で比較
| 比較項目 | IEnumerable<T> | List<T> |
|---|---|---|
| 主な役割 | 要素を順番に取り出す | 要素を保持して操作する |
| 実体 | インターフェース | 具体的なコレクション |
| foreach | 使える | 使える |
| Add | 使えない | 使える |
| Remove | 使えない | 使える |
| インデックスアクセス | 基本的に使えない | 使える |
| Count | Count()を使う | Countプロパティを使える |
| LINQ | 使える | 使える |
| 遅延実行 | 起こることがある | List自体は実体化済み |
| 向いている用途 | 読み取り、列挙、戻り値、引数 | 追加、削除、更新、ランダムアクセス |
IEnumerableは「できることをあえて少なくした型」ともいえます。できることが少ない分、配列、List、LINQ結果など、さまざまなデータを柔軟に受け取れます。
2-6. IEnumerableをListに変換するToList()の使い方
IEnumerable<T>をList<T>に変換したい場合は、ToList()を使います。
C#IEnumerable<int> numbers = Enumerable.Range(1, 5);
List<int> list = numbers.ToList();
LINQの結果をListとして固定したい場合にもよく使います。
C#var scores = new List<int> { 60, 80, 90, 40, 100 };
IEnumerable<int> highScores = scores.Where(x => x >= 80);
List<int> result = highScores.ToList();
ToList()を呼ぶと、その時点で列挙が実行され、結果がListに格納されます。
つまり、遅延実行されていた処理を即時実行し、結果を確定する操作です。
2-7. ListをIEnumerableとして受け取るメリット
メソッドの引数でList<T>ではなくIEnumerable<T>を使うと、受け取れるデータの幅が広がります。
C#void PrintNames(IEnumerable<string> names)
{
foreach (var name in names)
{
Console.WriteLine(name);
}
}
このメソッドには、Listを渡せます。
C#var list = new List<string> { "Alice", "Bob" };
PrintNames(list);
配列も渡せます。
C#string[] array = { "Charlie", "Dave" };
PrintNames(array);
LINQの結果も渡せます。
C#var filtered = list.Where(x => x.StartsWith("A"));
PrintNames(filtered);
このように、メソッド側が「順番に読めればよい」と考えているなら、List<T>ではなくIEnumerable<T>で受け取る方が柔軟です。
3. IEnumerableの基本的な使い方
3-1. foreachで要素を順番に処理する
IEnumerableの最も基本的な使い方は、foreachで要素を順番に処理することです。
C#IEnumerable<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
foreach (var number in numbers)
{
Console.WriteLine(number);
}
IEnumerable<int>は「int型の要素を順番に取り出せるもの」です。
そのため、foreachの中では各要素をintとして扱えます。
C#foreach (int number in numbers)
{
Console.WriteLine(number * 2);
}
Listでも配列でもLINQの結果でも、IEnumerable<T>として扱えば同じように処理できます。
3-2. メソッドの戻り値にIEnumerable<T>を使う
メソッドの戻り値としてIEnumerable<T>を使うこともよくあります。
C#IEnumerable<int> GetEvenNumbers()
{
return new List<int> { 2, 4, 6, 8, 10 };
}
呼び出し側は、戻り値をforeachで処理できます。
C#foreach (var number in GetEvenNumbers())
{
Console.WriteLine(number);
}
戻り値をIEnumerable<T>にすると、メソッドの内部実装を隠せます。
たとえば、内部ではListを返していても、呼び出し側は「順番に取り出せる」ということだけを知っていれば十分です。
C#IEnumerable<string> GetNames()
{
var names = new List<string> { "Alice", "Bob", "Charlie" };
return names;
}
将来的に内部実装を配列やLINQに変えても、呼び出し側への影響を小さくできます。
3-3. メソッドの引数にIEnumerable<T>を使う
引数にIEnumerable<T>を使うと、複数種類のコレクションを受け取れるようになります。
C#void PrintNumbers(IEnumerable<int> numbers)
{
foreach (var number in numbers)
{
Console.WriteLine(number);
}
}
このメソッドには、Listを渡せます。
C#PrintNumbers(new List<int> { 1, 2, 3 });
配列も渡せます。
C#PrintNumbers(new int[] { 4, 5, 6 });
LINQの結果も渡せます。
C#var numbers = new List<int> { 1, 2, 3, 4, 5 };
PrintNumbers(numbers.Where(x => x > 3));
メソッド内で追加や削除をしないのであれば、引数はList<T>よりIEnumerable<T>の方が柔軟です。
3-4. 配列・List・LINQ結果をIEnumerableで受け取る
IEnumerable<T>は、さまざまなデータを共通の型として受け取れます。
C#IEnumerable<int> fromArray = new int[] { 1, 2, 3 };
IEnumerable<int> fromList = new List<int> { 4, 5, 6 };
IEnumerable<int> fromLinq = fromList.Where(x => x >= 5);
どれもIEnumerable<int>として扱えるため、同じメソッドに渡せます。
C#void PrintAll(IEnumerable<int> values)
{
foreach (var value in values)
{
Console.WriteLine(value);
}
}
PrintAll(fromArray);
PrintAll(fromList);
PrintAll(fromLinq);
このように、IEnumerableを使うと、データの具体的な型に依存しないコードを書けます。
3-5. IEnumerableを使ったシンプルなサンプルコード
次の例では、名前の一覧をIEnumerable<string>として受け取り、1つずつ表示します。
C#using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
var names = new List<string>
{
"Alice",
"Bob",
"Charlie"
};
PrintNames(names);
}
static void PrintNames(IEnumerable<string> names)
{
foreach (var name in names)
{
Console.WriteLine(name);
}
}
}
PrintNamesメソッドは、List専用ではありません。
配列を渡すこともできます。
C#string[] names = { "Alice", "Bob", "Charlie" };
PrintNames(names);
このように、IEnumerable<T>を使うと、メソッドの再利用性が高くなります。
4. IEnumerableとLINQの関係
4-1. LINQはIEnumerableに対して使える便利な機能
LINQは、コレクションを簡潔に処理するための機能です。
Where、Select、OrderBy、GroupBy、First、Any、Countなど、多くのLINQメソッドはIEnumerable<T>に対して使えます。
C#var numbers = new List<int> { 1, 2, 3, 4, 5 };
var evenNumbers = numbers.Where(x => x % 2 == 0);
numbersはListですが、ListはIEnumerable<T>として扱えるため、LINQを使えます。
LINQを理解するうえでも、IEnumerableの理解は重要です。
4-2. Whereで条件に合う要素を絞り込む
Whereは、条件に合う要素だけを取り出すLINQメソッドです。
C#var numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
IEnumerable<int> evenNumbers = numbers.Where(x => x % 2 == 0);
foreach (var number in evenNumbers)
{
Console.WriteLine(number);
}
実行結果は次のようになります。
2
4
6
Whereの戻り値は、多くの場合IEnumerable<T>です。
つまり、絞り込んだ結果もまた「順番に取り出せるもの」として扱えます。
4-3. Selectで要素を変換する
Selectは、各要素を別の形に変換するLINQメソッドです。
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);
}
実行結果は次のようになります。
5
3
7
文字列の一覧から、文字数の一覧に変換しています。
オブジェクトの一覧から特定のプロパティだけを取り出す場合にもよく使います。
C#var users = new List<User>
{
new User { Name = "Alice", Age = 20 },
new User { Name = "Bob", Age = 30 }
};
IEnumerable<string> userNames = users.Select(user => user.Name);
Selectを使うと、必要な形にデータを変換しやすくなります。
4-4. OrderBy・GroupBy・First・Any・Countの基本
LINQには、WhereやSelect以外にも便利なメソッドがあります。
OrderByは、要素を並び替えます。
C#var numbers = new List<int> { 3, 1, 5, 2, 4 };
var ordered = numbers.OrderBy(x => x);
foreach (var number in ordered)
{
Console.WriteLine(number);
}
GroupByは、条件ごとにグループ化します。
C#var users = new List<User>
{
new User { Name = "Alice", Age = 20 },
new User { Name = "Bob", Age = 20 },
new User { Name = "Charlie", Age = 30 }
};
var groups = users.GroupBy(user => user.Age);
foreach (var group in groups)
{
Console.WriteLine($"Age: {group.Key}");
foreach (var user in group)
{
Console.WriteLine(user.Name);
}
}
Firstは、最初の要素を取得します。
C#int first = numbers.First();
条件を指定することもできます。
C#int firstEven = numbers.First(x => x % 2 == 0);
ただし、該当する要素がない場合は例外が発生します。存在しない可能性がある場合は、FirstOrDefaultを使います。
C#int value = numbers.FirstOrDefault(x => x > 100);
Anyは、条件に合う要素が存在するかを判定します。
C#bool hasEven = numbers.Any(x => x % 2 == 0);
Countは、要素数を数えます。
C#int count = numbers.Count();
条件に合う要素数を数えることもできます。
C#int evenCount = numbers.Count(x => x % 2 == 0);
4-5. LINQの戻り値がIEnumerableになるケース
LINQメソッドの多くは、戻り値としてIEnumerable<T>を返します。
C#var numbers = new List<int> { 1, 2, 3, 4, 5 };
IEnumerable<int> result = numbers
.Where(x => x >= 3)
.Select(x => x * 10);
この例では、3以上の数値を取り出し、それぞれ10倍しています。
30
40
50
この時点では、結果はListではありません。IEnumerable<int>です。
Listとして使いたい場合は、ToList()を呼びます。
C#List<int> list = result.ToList();
LINQを使っていると自然にIEnumerableが登場するため、C#でコレクション処理を書くなら理解しておきたい型です。
4-6. LINQを使うとコードが読みやすくなる理由
LINQを使うと、「何をしたいか」をコードで表現しやすくなります。
たとえば、80点以上の点数だけを取り出したい場合、foreachで書くと次のようになります。
C#var scores = new List<int> { 60, 80, 90, 40, 100 };
var highScores = new List<int>();
foreach (var score in scores)
{
if (score >= 80)
{
highScores.Add(score);
}
}
LINQを使うと短く書けます。
C#var highScores = scores.Where(score => score >= 80);
「80点以上に絞り込む」という意図が明確です。
さらに、変換や並び替えも組み合わせられます。
C#var result = scores
.Where(score => score >= 80)
.OrderByDescending(score => score)
.Select(score => $"Score: {score}");
LINQは、IEnumerableと組み合わせることで、読みやすく保守しやすいコードを書く助けになります。
4-7. LINQを使うときの注意点
LINQは便利ですが、いくつか注意点があります。
まず、遅延実行されるメソッドが多い点です。
C#var numbers = new List<int> { 1, 2, 3 };
var query = numbers.Where(x => x >= 2);
numbers.Add(4);
foreach (var number in query)
{
Console.WriteLine(number);
}
この結果には、後から追加した4も含まれます。
2
3
4
これは、Whereの処理が定義された時点ではなく、foreachで列挙された時点で実行されるためです。
また、Count()やToList()などをむやみに使うと、不要な列挙やメモリ使用が発生することがあります。
LINQを書くときは、「この処理はいつ実行されるのか」「何回列挙されるのか」を意識すると、バグやパフォーマンス問題を避けやすくなります。
5. IEnumerableの遅延実行とは?
5-1. 遅延実行は「必要になるまで処理しない」仕組み
IEnumerableを理解するうえで重要なのが、遅延実行です。
遅延実行とは、処理を定義した時点では実行せず、実際に要素が必要になったタイミングで実行する仕組みです。
たとえば、次のコードを見てください。
C#var numbers = new List<int> { 1, 2, 3, 4, 5 };
var query = numbers.Where(x =>
{
Console.WriteLine($"Check: {x}");
return x % 2 == 0;
});
この時点では、Console.WriteLineは実行されません。
queryを作っただけでは、まだ要素を取り出していないからです。
5-2. foreachしたタイミングで処理が実行される
遅延実行されるLINQクエリは、foreachなどで列挙したタイミングで実行されます。
C#foreach (var number in query)
{
Console.WriteLine($"Result: {number}");
}
このとき初めて、Whereの条件判定が行われます。
実行イメージは次のようになります。
Check: 1
Check: 2
Result: 2
Check: 3
Check: 4
Result: 4
Check: 5
Whereは最初にすべての結果を作るのではなく、1つずつ確認しながら必要な要素を返します。
これがIEnumerableとLINQでよく出てくる遅延実行です。
5-3. ToList()・ToArray()で即時実行される
遅延実行されるIEnumerableでも、ToList()やToArray()を呼ぶと、その時点で処理が実行されます。
C#var numbers = new List<int> { 1, 2, 3, 4, 5 };
var list = numbers
.Where(x =>
{
Console.WriteLine($"Check: {x}");
return x % 2 == 0;
})
.ToList();
この場合、ToList()を呼んだ時点でWhereが実行されます。
ToArray()も同様です。
C#int[] array = numbers.Where(x => x % 2 == 0).ToArray();
ToList()やToArray()は、遅延実行の結果をその場で確定させたいときに使います。
5-4. 遅延実行のメリット
遅延実行にはいくつかのメリットがあります。
まず、必要な分だけ処理できることです。
C#var result = numbers
.Where(x => x % 2 == 0)
.First();
この場合、最初の偶数が見つかった時点で処理を終えられます。すべての要素を最後まで処理する必要がない場合、効率的です。
また、大量データを扱うときに、すべての結果を一度にメモリに持たずに済むことがあります。
C#IEnumerable<int> values = Enumerable.Range(1, 1000000)
.Where(x => x % 2 == 0);
このような処理では、必要なタイミングで1つずつ値を取り出せます。
5-5. 遅延実行のデメリット
遅延実行は便利ですが、初心者がつまずきやすい原因にもなります。
代表的なデメリットは、「思ったタイミングで処理が実行されない」ことです。
C#var numbers = new List<int> { 1, 2, 3 };
var query = numbers.Where(x => x >= 2);
numbers.Add(4);
このあと列挙すると、4も結果に含まれます。
C#foreach (var number in query)
{
Console.WriteLine(number);
}
実行結果は次のようになります。
2
3
4
queryを作った時点の結果を固定したい場合は、ToList()を使う必要があります。
C#var query = numbers.Where(x => x >= 2).ToList();
5-6. 同じIEnumerableを複数回列挙するときの注意点
IEnumerableは、列挙するたびに処理が再実行されることがあります。
C#var numbers = new List<int> { 1, 2, 3, 4, 5 };
var query = numbers.Where(x =>
{
Console.WriteLine($"Check: {x}");
return x % 2 == 0;
});
Console.WriteLine(query.Count());
Console.WriteLine(query.Sum());
このコードでは、Count()で1回、Sum()で1回、合計2回列挙される可能性があります。
処理が軽ければ問題にならないこともありますが、データベースアクセス、ファイル読み込み、複雑な計算などが絡む場合は注意が必要です。
複数回使う結果は、List化しておくと安全です。
C#var list = query.ToList();
Console.WriteLine(list.Count);
Console.WriteLine(list.Sum());
5-7. 遅延実行で起こりやすいバグの例
遅延実行でよくあるバグは、「クエリ作成後に元のコレクションを変更したら、結果も変わってしまう」というものです。
C#var names = new List<string> { "Alice", "Bob", "Charlie" };
var query = names.Where(name => name.StartsWith("A"));
names.Add("Alex");
foreach (var name in query)
{
Console.WriteLine(name);
}
実行結果は次のようになります。
Alice
Alex
queryを作った時点ではAlexは存在しませんでした。しかし、列挙時点ではListに追加されているため、結果に含まれます。
作成時点の結果を固定したい場合は、次のように書きます。
C#var query = names.Where(name => name.StartsWith("A")).ToList();
この場合、ToList()を呼んだ時点で結果が確定します。
6. IEnumerable・IEnumerator・yield returnの関係
6-1. IEnumerableとIEnumeratorの違い
IEnumerableと似た名前に、IEnumeratorがあります。
この2つは役割が違います。
IEnumerable<T>は、「列挙できるもの」を表します。
C#IEnumerable<int> numbers;
一方、IEnumerator<T>は、「実際に列挙を進めるためのもの」です。
C#IEnumerator<int> enumerator;
イメージとしては、IEnumerableが「本棚」、IEnumeratorが「本棚を順番に見ていく人」です。
IEnumerableからGetEnumerator()を呼ぶと、IEnumeratorを取得できます。
C#IEnumerator<int> enumerator = numbers.GetEnumerator();
6-2. GetEnumerator()・MoveNext()・Currentの役割
IEnumerable<T>には、GetEnumerator()があります。
C#IEnumerator<T> GetEnumerator();
GetEnumerator()は、要素を順番に取り出すための列挙子を返します。
IEnumerator<T>には、主に次のようなメンバーがあります。
C#MoveNext()
Current
MoveNext()は、次の要素へ進みます。次の要素があればtrue、なければfalseを返します。
Currentは、現在の要素を取得します。
C#var numbers = new List<int> { 1, 2, 3 };
var enumerator = numbers.GetEnumerator();
while (enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
}
実行結果は次のようになります。
1
2
3
普段はforeachを使うため、IEnumeratorを直接扱うことは多くありません。しかし、foreachの裏側を理解するには重要です。
6-3. foreachの裏側で起きていること
foreachは、内部的にGetEnumerator()、MoveNext()、Currentを使って要素を処理します。
次のコードがあるとします。
C#foreach (var number in numbers)
{
Console.WriteLine(number);
}
イメージとしては、次のような処理に近いです。
C#var enumerator = numbers.GetEnumerator();
while (enumerator.MoveNext())
{
var number = enumerator.Current;
Console.WriteLine(number);
}
つまり、foreachは魔法の構文ではなく、IEnumerableとIEnumeratorの仕組みを使って動いています。
この仕組みがあるため、List、配列、Dictionary、LINQ結果などを同じようにforeachで処理できます。
6-4. yield returnでIEnumerableを簡単に作る
yield returnを使うと、IEnumerable<T>を簡単に作れます。
C#IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
yield return 3;
}
このメソッドは、IEnumerable<int>を返します。
C#foreach (var number in GetNumbers())
{
Console.WriteLine(number);
}
実行結果は次のようになります。
1
2
3
yield returnを使うと、自分でIEnumeratorを実装しなくても、順番に値を返す処理を書けます。
6-5. yield returnと遅延実行の関係
yield returnを使ったメソッドも遅延実行されます。
C#IEnumerable<int> GetNumbers()
{
Console.WriteLine("Start");
yield return 1;
Console.WriteLine("Middle");
yield return 2;
Console.WriteLine("End");
yield return 3;
}
次のように呼び出しても、この時点では中身は実行されません。
C#var numbers = GetNumbers();
foreachで列挙したタイミングで実行されます。
C#foreach (var number in numbers)
{
Console.WriteLine($"Value: {number}");
}
実行イメージは次のようになります。
Start
Value: 1
Middle
Value: 2
End
Value: 3
yield returnは、値を1つずつ必要なタイミングで返します。そのため、遅延実行と相性が良い機能です。
6-6. yield breakの使い方
yield breakを使うと、列挙を途中で終了できます。
C#IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
yield break;
yield return 3;
}
この場合、3は返されません。
C#foreach (var number in GetNumbers())
{
Console.WriteLine(number);
}
実行結果は次のようになります。
1
2
条件に応じて列挙を終了したい場合にも使えます。
C#IEnumerable<int> TakeUntilNegative(IEnumerable<int> numbers)
{
foreach (var number in numbers)
{
if (number < 0)
{
yield break;
}
yield return number;
}
}
負の数が出てきた時点で列挙を終了する処理です。
7. IEnumerableを使うべき場面・Listを使うべき場面
7-1. IEnumerableを使うべきケース
IEnumerable<T>は、次のような場面で使うと効果的です。
メソッド内で要素を順番に読むだけの場合です。
C#void PrintItems(IEnumerable<string> items)
{
foreach (var item in items)
{
Console.WriteLine(item);
}
}
このメソッドは、Listでも配列でもLINQ結果でも受け取れます。
また、戻り値として「列挙できる結果」を返したい場合にも向いています。
C#IEnumerable<int> GetEvenNumbers(IEnumerable<int> numbers)
{
return numbers.Where(x => x % 2 == 0);
}
内部でListを使っているか、配列を使っているか、LINQを使っているかを呼び出し側に意識させたくない場合にも便利です。
7-2. Listを使うべきケース
List<T>は、要素を追加・削除・更新したい場合に向いています。
C#var names = new List<string>();
names.Add("Alice");
names.Add("Bob");
names.Remove("Alice");
インデックスでアクセスしたい場合もListが便利です。
C#Console.WriteLine(names[0]);
names[0] = "Charlie";
件数を頻繁に取得する場合も、ListのCountプロパティを使うとわかりやすく高速です。
C#int count = names.Count;
データをメモリ上に保持し、何度も操作するならListを選ぶとよいでしょう。
7-3. APIやメソッド設計でIEnumerableを使うメリット
APIやメソッドの引数にIEnumerable<T>を使うと、呼び出し側の自由度が上がります。
C#void SendEmails(IEnumerable<string> emailAddresses)
{
foreach (var email in emailAddresses)
{
Console.WriteLine($"Send to: {email}");
}
}
このメソッドは、List、配列、LINQ結果など、さまざまな形のデータを受け取れます。
C#SendEmails(new List<string> { "a@example.com", "b@example.com" });
SendEmails(new string[] { "c@example.com", "d@example.com" });
メソッド側が必要としているのは「順番に取り出せること」だけです。その場合、具体的なList<T>に限定する必要はありません。
このように、引数をIEnumerable<T>にすると、疎結合で使いやすい設計になります。
7-4. 変更が必要な場合はListを使う
メソッド内で要素を追加・削除したい場合は、IEnumerable<T>ではなくList<T>を使います。
C#void AddDefaultName(List<string> names)
{
names.Add("Default");
}
IEnumerable<T>ではAddが使えません。
C#void AddDefaultName(IEnumerable<string> names)
{
// names.Add("Default"); // コンパイルエラー
}
ただし、引数として受け取ったIEnumerable<T>をListに変換してから操作することはできます。
C#void AddDefaultName(IEnumerable<string> names)
{
var list = names.ToList();
list.Add("Default");
}
この場合、元のコレクションを変更しているのではなく、新しいListを作っている点に注意しましょう。
7-5. 結果を固定したい場合はToList()を使う
LINQの結果を後から変わらない形で保持したい場合は、ToList()を使います。
C#var names = new List<string> { "Alice", "Bob", "Charlie" };
var result = names
.Where(name => name.Contains("a", StringComparison.OrdinalIgnoreCase))
.ToList();
ToList()を呼ぶと、その時点で結果が確定します。
その後、元のListを変更しても、resultの中身は変わりません。
C#names.Add("Amanda");
foreach (var name in result)
{
Console.WriteLine(name);
}
遅延実行による意図しない結果変更を避けたい場合は、ToList()で固定するのが有効です。
7-6. IEnumerable・List・Arrayの使い分け
IEnumerable<T>、List<T>、配列は、それぞれ得意な用途が異なります。
| 型 | 向いている場面 |
|---|---|
| IEnumerable<T> | 順番に読み取るだけ、柔軟な引数・戻り値にしたい |
| List<T> | 追加・削除・更新・インデックスアクセスをしたい |
| Array | 要素数が固定で、シンプルに扱いたい |
たとえば、メソッドの引数ではIEnumerable<T>が便利です。
C#void Print(IEnumerable<int> numbers)
{
foreach (var number in numbers)
{
Console.WriteLine(number);
}
}
一方、内部でデータを組み立てる場合はListが便利です。
C#var numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);
要素数が固定で変わらない場合は配列も選択肢になります。
C#int[] numbers = { 1, 2, 3 };
重要なのは、「何をしたいか」によって型を選ぶことです。
8. IEnumerableでよくあるエラー・つまずきポイント
8-1. IEnumerableにAddできない
初心者がよく遭遇するのが、IEnumerableにAddできないという問題です。
C#IEnumerable<int> numbers = new List<int> { 1, 2, 3 };
// numbers.Add(4); // コンパイルエラー
中身がListであっても、変数の型がIEnumerable<int>であれば、IEnumerable<int>に定義された機能しか使えません。
追加したい場合は、型をListにします。
C#List<int> numbers = new List<int> { 1, 2, 3 };
numbers.Add(4);
または、IEnumerableをListに変換します。
C#IEnumerable<int> numbers = new List<int> { 1, 2, 3 };
List<int> list = numbers.ToList();
list.Add(4);
8-2. IEnumerableでインデックスアクセスできない
IEnumerable<T>では、基本的にインデックスアクセスができません。
C#IEnumerable<string> names = new List<string> { "Alice", "Bob" };
// Console.WriteLine(names[0]); // コンパイルエラー
インデックスでアクセスしたい場合は、Listや配列を使います。
C#List<string> names = new List<string> { "Alice", "Bob" };
Console.WriteLine(names[0]);
LINQのElementAt()を使う方法もあります。
C#IEnumerable<string> names = new List<string> { "Alice", "Bob" };
Console.WriteLine(names.ElementAt(0));
ただし、ElementAt()は対象によっては先頭から順番に列挙するため、頻繁に使うと効率が悪くなることがあります。
何度もインデックスアクセスしたい場合は、Listに変換した方がよいでしょう。
C#var list = names.ToList();
Console.WriteLine(list[0]);
8-3. Count()を何度も呼ぶと遅くなることがある
IEnumerable<T>では、LINQのCount()を使って件数を取得できます。
C#int count = numbers.Count();
ただし、IEnumerableの種類によっては、Count()が毎回すべての要素を列挙して数えることがあります。
C#if (numbers.Count() > 0)
{
Console.WriteLine(numbers.Count());
}
このように何度もCount()を呼ぶと、無駄な列挙が発生する可能性があります。
存在確認だけなら、Any()を使う方が適していることが多いです。
C#if (numbers.Any())
{
Console.WriteLine("要素があります");
}
件数を何度も使う場合は、一度変数に入れるか、List化を検討します。
C#var list = numbers.ToList();
if (list.Count > 0)
{
Console.WriteLine(list.Count);
}
8-4. LINQの結果が思ったタイミングで実行されない
LINQのWhereやSelectは遅延実行されるため、クエリを作った時点では処理されないことがあります。
C#var numbers = new List<int> { 1, 2, 3 };
var query = numbers.Where(x =>
{
Console.WriteLine(x);
return x > 1;
});
この時点では、まだ何も表示されません。
実際に表示されるのは、foreachなどで列挙したタイミングです。
C#foreach (var number in query)
{
Console.WriteLine($"Result: {number}");
}
処理をすぐに実行したい場合は、ToList()やToArray()を使います。
C#var result = numbers.Where(x => x > 1).ToList();
8-5. コレクション変更後に結果が変わってしまう
遅延実行により、元のコレクションを変更した後に列挙すると、結果が変わることがあります。
C#var numbers = new List<int> { 1, 2, 3 };
var query = numbers.Where(x => x >= 2);
numbers.Add(4);
foreach (var number in query)
{
Console.WriteLine(number);
}
実行結果は次のようになります。
2
3
4
queryを作った時点では4はありませんでしたが、列挙時点では追加されているため結果に含まれます。
作成時点の結果を固定したい場合は、ToList()を使います。
C#var query = numbers.Where(x => x >= 2).ToList();
8-6. nullのIEnumerableを扱うときの注意点
IEnumerable<T>がnullの場合、foreachすると例外が発生します。
C#IEnumerable<int> numbers = null;
foreach (var number in numbers)
{
Console.WriteLine(number);
}
このコードはNullReferenceExceptionになります。
安全に扱うには、nullチェックを行います。
C#if (numbers != null)
{
foreach (var number in numbers)
{
Console.WriteLine(number);
}
}
または、空のIEnumerableに置き換える方法もあります。
C#foreach (var number in numbers ?? Enumerable.Empty<int>())
{
Console.WriteLine(number);
}
メソッドの戻り値としては、nullを返すよりも空のコレクションを返す方が扱いやすいです。
C#IEnumerable<int> GetNumbers()
{
return Enumerable.Empty<int>();
}
9. IEnumerableとIQueryableの違い
9-1. IEnumerableはメモリ上のデータ処理に向いている
IEnumerable<T>は、主にメモリ上のデータを列挙して処理する場面に向いています。
C#var users = new List<User>
{
new User { Name = "Alice", Age = 20 },
new User { Name = "Bob", Age = 30 }
};
var result = users.Where(user => user.Age >= 20);
この場合、Listに入っているデータをメモリ上で処理しています。
配列やList、すでに取得済みのデータに対してLINQを使う場合は、IEnumerableで十分なことが多いです。
9-2. IQueryableはデータベースクエリに向いている
IQueryable<T>は、データベースなど外部データソースに対するクエリを表すときによく使われます。
Entity Frameworkなどでは、DBテーブルに対する問い合わせ結果がIQueryable<T>として扱われることがあります。
C#IQueryable<User> query = dbContext.Users.Where(user => user.Age >= 20);
IQueryableは、LINQの式をSQLなどに変換して、データベース側で実行できる点が特徴です。
つまり、IEnumerableが「メモリ上で処理する」イメージなのに対し、IQueryableは「データベースなどに問い合わせる処理を組み立てる」イメージです。
9-3. Entity FrameworkでIEnumerableとIQueryableを間違えるリスク
Entity FrameworkでIEnumerableとIQueryableを間違えると、パフォーマンスに影響することがあります。
たとえば、次のようにIQueryableのまま条件を追加すると、条件がSQLに反映されることが期待できます。
C#var query = dbContext.Users.Where(user => user.Age >= 20);
var result = query.Where(user => user.Name.StartsWith("A"));
一方、途中でAsEnumerable()やToList()を呼ぶと、それ以降の処理がメモリ上で行われることがあります。
C#var users = dbContext.Users
.AsEnumerable()
.Where(user => user.Age >= 20);
この場合、データの取得範囲やタイミングによっては、必要以上のデータをアプリケーション側に読み込んでから絞り込むことになり、効率が悪くなる可能性があります。
DB処理では、できるだけデータベース側で絞り込むことが重要です。
9-4. AsEnumerable()を使うときの注意点
AsEnumerable()は、対象をIEnumerable<T>として扱うためのメソッドです。
C#var result = dbContext.Users
.AsEnumerable()
.Where(user => SomeCSharpMethod(user.Name));
AsEnumerable()を使うと、以降のLINQ処理はC#側で実行されることがあります。
これは、SQLに変換できないC#のメソッドを使いたい場合には便利です。
しかし、早い段階でAsEnumerable()を呼ぶと、DBから大量のデータを取得してからメモリ上で絞り込むことになりかねません。
よくない例です。
C#var result = dbContext.Users
.AsEnumerable()
.Where(user => user.Age >= 20)
.ToList();
可能であれば、DBで実行できる条件はAsEnumerable()の前に書きます。
C#var result = dbContext.Users
.Where(user => user.Age >= 20)
.AsEnumerable()
.Where(user => SomeCSharpMethod(user.Name))
.ToList();
9-5. WebアプリやDB処理での使い分け
WebアプリやDB処理では、IEnumerableとIQueryableの使い分けが重要です。
メモリ上のデータを処理するならIEnumerable<T>で問題ありません。
C#IEnumerable<User> users = userList.Where(user => user.IsActive);
DBに問い合わせる段階では、IQueryable<T>のまま条件を組み立てる方が効率的なことがあります。
C#IQueryable<User> query = dbContext.Users;
query = query.Where(user => user.IsActive);
query = query.Where(user => user.Age >= 20);
var users = query.ToList();
このように、DBから取得する前に条件を組み立て、最後にToList()で実行する流れがよく使われます。
ただし、アプリケーションの外側にIQueryableをそのまま公開すると、どこでクエリが実行されるのかわかりにくくなることもあります。
実務では、DBアクセス層ではIQueryableを活用し、サービス層や画面側にはIEnumerableやListとして渡すなど、責務を分けることが大切です。
10. IEnumerableのパフォーマンスを意識した使い方
10-1. 不要なToList()を避ける
ToList()は便利ですが、必要がない場面で使うと、余計なメモリ使用や処理時間が発生します。
C#var result = numbers
.Where(x => x % 2 == 0)
.ToList();
foreach (var number in result)
{
Console.WriteLine(number);
}
このコードでは、foreachで一度処理するだけなら、必ずしもList化する必要はありません。
C#var result = numbers.Where(x => x % 2 == 0);
foreach (var number in result)
{
Console.WriteLine(number);
}
List化すると、その時点ですべての結果をメモリ上に保持します。
結果を何度も使う、件数を頻繁に確認する、後から元データが変わっても結果を固定したい、といった理由がある場合にToList()を使うとよいでしょう。
10-2. 複数回列挙するならList化を検討する
同じIEnumerableを複数回使う場合は、処理が毎回実行される可能性があります。
C#var query = numbers.Where(x =>
{
Console.WriteLine($"Check: {x}");
return x % 2 == 0;
});
int count = query.Count();
int sum = query.Sum();
この場合、Count()とSum()で2回列挙されます。
一度List化しておけば、処理結果を再利用できます。
C#var list = query.ToList();
int count = list.Count;
int sum = list.Sum();
特に、重い計算、ファイルアクセス、DBアクセスに関係するIEnumerableでは、複数回列挙に注意が必要です。
10-3. WhereやSelectの順番を工夫する
LINQでは、WhereやSelectの順番によって効率が変わることがあります。
たとえば、先に絞り込んでから変換する方が効率的な場合があります。
C#var result = users
.Where(user => user.IsActive)
.Select(user => CreateViewModel(user));
この場合、アクティブなユーザーだけをViewModelに変換します。
逆に、先に全件を変換してから絞り込むと、不要な変換が発生する可能性があります。
C#var result = users
.Select(user => CreateViewModel(user))
.Where(viewModel => viewModel.IsActive);
必ずしも常に前者が正しいわけではありませんが、基本的には「件数を減らせる処理を先に行う」と効率がよくなりやすいです。
10-4. 大量データを扱うときの注意点
大量データを扱う場合、IEnumerableは便利ですが、注意も必要です。
たとえば、次のように大量データを一気にList化すると、メモリを多く使います。
C#var list = Enumerable.Range(1, 10000000).ToList();
一方、必要な分だけ列挙するなら、IEnumerableの遅延実行が役立ちます。
C#var values = Enumerable.Range(1, 10000000)
.Where(x => x % 2 == 0);
foreach (var value in values.Take(10))
{
Console.WriteLine(value);
}
この場合、すべての偶数をListに格納するのではなく、必要な分だけ処理できます。
ただし、DBや外部APIから大量データを取得する場合は、IEnumerableだけでなく、ページングやストリーミング、非同期処理なども検討する必要があります。
10-5. 遅延実行とメモリ効率の考え方
遅延実行は、すべての結果を一度に作らず、必要なタイミングで処理できるため、メモリ効率がよくなることがあります。
C#IEnumerable<int> numbers = Enumerable.Range(1, 1000000)
.Where(x => x % 2 == 0)
.Select(x => x * 10);
この時点では、結果のListが作られているわけではありません。
必要になったときに、1つずつ処理されます。
C#foreach (var number in numbers)
{
Console.WriteLine(number);
}
一方で、結果を何度も使う場合や、結果を固定したい場合はList化した方がよいこともあります。
C#var list = numbers.ToList();
つまり、IEnumerableのパフォーマンスを考えるときは、次の2点が重要です。
| 観点 | 考えること |
|---|---|
| メモリ | すべてList化する必要があるか |
| 実行回数 | 同じIEnumerableを何度も列挙していないか |
遅延実行は強力ですが、万能ではありません。処理のタイミングと回数を意識して使うことが大切です。
11. IEnumerableの理解を深める実践コード
11-1. ListをIEnumerableとして扱う例
ListはIEnumerable<T>として扱えます。
C#using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<string> names = new List<string>
{
"Alice",
"Bob",
"Charlie"
};
PrintNames(names);
}
static void PrintNames(IEnumerable<string> names)
{
foreach (var name in names)
{
Console.WriteLine(name);
}
}
}
PrintNamesの引数はIEnumerable<string>ですが、Listを問題なく渡せます。
このように、メソッドが要素を読むだけなら、具体的なListではなくIEnumerableで受け取ると柔軟です。
11-2. LINQで条件抽出する例
次は、LINQのWhereを使って条件に合う要素を取り出す例です。
C#using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
var scores = new List<int> { 45, 80, 95, 60, 100 };
IEnumerable<int> highScores = scores.Where(score => score >= 80);
foreach (var score in highScores)
{
Console.WriteLine(score);
}
}
}
実行結果は次のようになります。
80
95
100
Whereの戻り値はIEnumerable<int>として扱えます。
11-3. ToList()で結果を確定する例
LINQの結果を固定したい場合は、ToList()を使います。
C#using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
var numbers = new List<int> { 1, 2, 3 };
var query = numbers.Where(x => x >= 2).ToList();
numbers.Add(4);
foreach (var number in query)
{
Console.WriteLine(number);
}
}
}
実行結果は次のようになります。
2
3
ToList()を呼んだ時点で結果が確定しているため、後から追加した4は含まれません。
11-4. yield returnで独自のIEnumerableを作る例
yield returnを使うと、自分でIEnumerable<T>を返すメソッドを簡単に作れます。
C#using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
foreach (var number in GetNumbers())
{
Console.WriteLine(number);
}
}
static IEnumerable<int> GetNumbers()
{
yield return 10;
yield return 20;
yield return 30;
}
}
実行結果は次のようになります。
10
20
30
yield returnを使うと、Listを作って返す必要がない場合があります。
C#static IEnumerable<int> GetNumbers()
{
var list = new List<int>();
list.Add(10);
list.Add(20);
list.Add(30);
return list;
}
このようにListを作る代わりに、yield returnで1つずつ返せます。
11-5. 遅延実行の動きを確認する例
最後に、遅延実行の動きを確認するコードです。
C#using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var query = numbers.Where(x =>
{
Console.WriteLine($"Check: {x}");
return x % 2 == 0;
});
Console.WriteLine("クエリ作成後");
foreach (var number in query)
{
Console.WriteLine($"Result: {number}");
}
}
}
実行結果は次のようになります。
クエリ作成後
Check: 1
Check: 2
Result: 2
Check: 3
Check: 4
Result: 4
Check: 5
Whereの処理は、クエリを作った時点では実行されていません。foreachで列挙したタイミングで初めて実行されています。
これがIEnumerableとLINQで重要な遅延実行の動きです。
12. C#のIEnumerableに関するよくある質問
12-1. IEnumerableとListはどちらを使えばいい?
要素を順番に読むだけなら、IEnumerable<T>を使うと柔軟です。
C#void Print(IEnumerable<int> numbers)
{
foreach (var number in numbers)
{
Console.WriteLine(number);
}
}
要素の追加・削除・更新・インデックスアクセスが必要なら、List<T>を使います。
C#var numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
メソッドの引数ではIEnumerable<T>、内部でデータを組み立てるときはList<T>という使い分けがよくあります。
12-2. IEnumerableは読み取り専用なの?
IEnumerable<T>自体は、要素を順番に取り出す機能だけを提供します。そのため、IEnumerable<T>経由では追加や削除はできません。
ただし、IEnumerable<T>が必ず完全な読み取り専用という意味ではありません。
C#var list = new List<int> { 1, 2, 3 };
IEnumerable<int> numbers = list;
list.Add(4);
このように、元のListを変更すれば、IEnumerableとして見える結果も変わることがあります。
つまり、IEnumerable<T>は「変更できないコレクション」ではなく、「変更するメソッドを公開していない列挙用の型」と考えるとよいでしょう。
12-3. IEnumerableはなぜAddできない?
IEnumerable<T>は、要素を順番に取り出すためのインターフェースだからです。
Addは「コレクションに要素を追加する」操作ですが、すべてのIEnumerableが追加可能とは限りません。
たとえば、配列はサイズ固定です。
C#int[] numbers = { 1, 2, 3 };
LINQの結果も、追加先の実体を持っているとは限りません。
C#var query = numbers.Where(x => x > 1);
そのため、IEnumerable<T>にはAddが定義されていません。
追加したい場合は、List<T>を使います。
C#var list = numbers.ToList();
list.Add(4);
12-4. IEnumerableと配列の違いは?
配列は、要素数が固定された具体的なデータ構造です。
C#int[] numbers = { 1, 2, 3 };
インデックスでアクセスできます。
C#Console.WriteLine(numbers[0]);
一方、IEnumerable<T>は「順番に取り出せる」という性質を表すインターフェースです。
C#IEnumerable<int> values = numbers;
IEnumerable<T>として扱うと、インデックスアクセスや要素数固定といった配列固有の機能は見えなくなります。
配列はIEnumerable<T>として扱えますが、IEnumerable<T>が必ず配列であるとは限りません。
12-5. IEnumerableとCollectionの違いは?
IEnumerable<T>は、要素を順番に取り出すための最小限のインターフェースです。
Collection<T>やICollection<T>は、要素数の取得や追加・削除など、より多くの操作を提供します。
たとえば、ICollection<T>にはCountやAddなどがあります。
C#ICollection<int> numbers = new List<int>();
numbers.Add(1);
Console.WriteLine(numbers.Count);
一方、IEnumerable<T>ではAddは使えません。
C#IEnumerable<int> values = numbers;
// values.Add(2); // コンパイルエラー
必要な機能が少ないほど、より抽象的な型を使えます。読むだけならIEnumerable<T>、追加や削除も必要ならICollection<T>やList<T>を検討します。
12-6. IEnumerableをListに変換してもよい?
必要がある場合は、IEnumerable<T>をListに変換して問題ありません。
C#IEnumerable<int> numbers = Enumerable.Range(1, 5);
List<int> list = numbers.ToList();
次のような場合は、ToList()が有効です。
結果を固定したい場合。
C#var result = numbers.Where(x => x > 2).ToList();
複数回列挙したい場合。
C#var list = numbers.Where(x => x > 2).ToList();
Console.WriteLine(list.Count);
Console.WriteLine(list.Sum());
追加や削除をしたい場合。
C#var list = numbers.ToList();
list.Add(10);
ただし、不要なToList()はメモリ使用量を増やすことがあります。必要な理由があるときに使うのがポイントです。
12-7. IEnumerableは初心者でも使うべき?
C#を学ぶ初心者でも、IEnumerableはぜひ理解しておきたい型です。
最初から難しい内部実装まで覚える必要はありません。まずは次の3点を押さえるだけでも十分です。
| ポイント | 内容 |
|---|---|
| IEnumerableは順番に取り出せるもの | foreachで処理できる |
| Listとは役割が違う | AddやRemoveはできない |
| LINQと関係が深い | WhereやSelectの結果でよく使う |
C#では、LINQやメソッド設計でIEnumerable<T>が頻繁に登場します。
初心者のうちは、「Listより機能が少ない型」ではなく、「いろいろなコレクションを共通して扱うための型」と考えると理解しやすくなります。
まとめ
IEnumerableは、C#で「要素を順番に取り出せること」を表す重要なインターフェースです。
List、配列、Dictionary、LINQの結果など、多くのコレクションはIEnumerableとして扱えます。
IEnumerable<T>を使うと、具体的なコレクション型に依存せず、柔軟なコードを書けます。
一方で、IEnumerable<T>にはAddやRemove、インデックスアクセスなどの機能はありません。要素を追加・削除・更新したい場合はList<T>を使います。
また、LINQと組み合わせると、WhereやSelectなどの処理を簡潔に書けますが、遅延実行には注意が必要です。
特に重要なポイントは次のとおりです。
| ポイント | 内容 |
|---|---|
| IEnumerable | 順番に要素を取り出せるもの |
| List | 要素を保持し、追加・削除・更新できるもの |
| LINQ | IEnumerableに対して使える便利な処理 |
| 遅延実行 | foreachやToList()まで処理が実行されないことがある |
| ToList() | IEnumerableの結果をListとして確定する |
| yield return | IEnumerableを簡単に作る構文 |
C#でコレクション処理を書くなら、IEnumerableの理解は避けて通れません。
まずは「foreachできるもの」「LINQの結果としてよく出てくるもの」「Listより抽象的な型」という感覚から始めると、実務コードも読みやすくなります。

