C#のIEnumerableとListの違い・使い分け・変換方法をわかりやすく解説

はじめに

C#でコレクションを扱っていると、IEnumerable<T>List<T>のどちらを使うべきか迷う場面がよくあります。

どちらも「複数のデータを扱う」ために使われますが、役割は同じではありません。IEnumerable<T>は主に「順番に取り出せること」を表すインターフェイスで、List<T>は「要素を保持し、追加・削除・インデックスアクセスなどの操作ができる具体的なコレクション」です。Microsoft Learnでも、IEnumerable<T>foreachで列挙できる基盤インターフェイス、List<T>は動的にサイズが増える配列を内部的に使うコレクションとして説明されています。Microsoft Learn+1

この記事では、C#のIEnumerableListの違い、使い分け、変換方法、注意点を初心者にもわかりやすく解説します。

1. C#のIEnumerableとListの違いを最初に結論で解説

1-1. IEnumerableは「順番に取り出せるもの」、Listは「要素を保持して操作できるコレクション」

最初に結論から言うと、IEnumerable<T>は「データを順番に取り出せること」を表す型です。

一方、List<T>は実際にデータを保持し、追加・削除・検索・並べ替え・インデックスアクセスなどができる具体的なコレクションです。

たとえば、次のようなイメージです。

C#
IEnumerable<string> names = GetNames();

この場合、namesは「順番に取り出せる文字列の集まり」です。ただし、IEnumerable<T>自体にはAddRemoveのような変更用メソッドはありません。

一方で、次のようにList<T>を使うと、要素を追加できます。

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

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

つまり、IEnumerable<T>は「読む・列挙する」ことに向いており、List<T>は「保持する・変更する・何度も使う」ことに向いています。

1-2. IEnumerableとListの主な違いを比較表で整理

IEnumerable<T>List<T>の違いをまとめると、次のようになります。

項目IEnumerable<T>List<T>
種類インターフェイス具体的なクラス
主な役割順番に列挙する要素を保持して操作する
foreachできるできる
Addできないできる
Removeできないできる
インデックスアクセス基本的にできないできる
Count直接は持たないCountプロパティを持つ
LINQとの相性非常に良い良い
遅延実行あり得る基本的にデータは保持済み
向いている用途読み取り、列挙、戻り値、引数追加・削除、ランダムアクセス、再利用

List<T>IEnumerable<T>を実装しているため、List<T>IEnumerable<T>として扱えます。Microsoft Learnでも、IEnumerable<T>List<T>Dictionary<TKey,TValue>など多くのコレクションの基盤インターフェイスであると説明されています。Microsoft Learn

1-3. 迷ったときの使い分け早見表

迷ったときは、次の基準で選ぶとわかりやすいです。

やりたいこと選ぶ型
ただ順番に処理したいIEnumerable<T>
LINQの結果を受け取りたいIEnumerable<T>
メソッドの戻り値として柔軟に返したいIEnumerable<T>
メソッドの引数として幅広いコレクションを受け取りたいIEnumerable<T>
要素を追加・削除したいList<T>
items[0]のようにアクセスしたいList<T>
要素数を頻繁に使いたいList<T>
同じ結果を何度もループしたいList<T>
LINQの結果を確定させたいToList()List<T>

基本方針としては、「入力や戻り値はIEnumerable<T>で柔軟に、内部で変更が必要ならList<T>を使う」と考えると実務でも扱いやすくなります。

2. IEnumerableとは何か

2-1. IEnumerableの基本的な役割

IEnumerable<T>は、C#で「列挙できるデータ」を表すためのインターフェイスです。

列挙とは、要素を1つずつ順番に取り出すことです。配列、List<T>、LINQの検索結果など、多くのデータ構造はIEnumerable<T>として扱えます。

たとえば、次のようなメソッドを考えます。

C#
IEnumerable<int> GetNumbers()
{
return new List<int> { 1, 2, 3, 4, 5 };
}

戻り値の型はIEnumerable<int>ですが、実際に返しているのはList<int>です。これは、List<T>IEnumerable<T>を実装しているためです。

このように、IEnumerable<T>を使うと、呼び出し側は「中身がListなのか、配列なのか、LINQの結果なのか」を気にせず、順番に処理できます。

2-2. foreachで列挙できる仕組み

IEnumerable<T>は、foreachで処理できる型です。

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

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

このコードでは、namesから要素を1つずつ取り出して表示しています。

IEnumerable<T>にはGetEnumerator()というメソッドがあり、このメソッドが列挙子を返します。C#のforeachは、この仕組みを使って要素を順番に取り出します。Microsoft Learnでも、IEnumerable<T>にはGetEnumerator()が定義され、foreachで列挙できると説明されています。Microsoft Learn

2-3. LINQとIEnumerableの関係

IEnumerable<T>はLINQと非常に相性が良い型です。

たとえば、次のようにWhereを使うと、条件に一致する要素だけを取り出せます。

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

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

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

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

C#
2
4

WhereSelectなど、多くのLINQメソッドはIEnumerable<T>を入力として受け取り、IEnumerable<T>を返します。そのため、LINQを使った処理ではIEnumerable<T>がよく登場します。

2-4. IEnumerableは読み取り・列挙に向いている

IEnumerable<T>は、データを読み取ったり、順番に処理したりする用途に向いています。

たとえば、一覧を画面に表示する、条件に合うデータだけを処理する、合計値を計算する、といったケースです。

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

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

C#
PrintNames(new List<string> { "Alice", "Bob" });
PrintNames(new string[] { "Tom", "Jerry" });

このように、引数をIEnumerable<T>にすると、呼び出し側がさまざまなコレクションを渡せるため、柔軟な設計になります。

2-5. 遅延実行とは何か

IEnumerable<T>を理解するうえで重要なのが「遅延実行」です。

遅延実行とは、LINQのクエリを作成した時点では処理を実行せず、実際にforeachなどで列挙されたタイミングで処理が実行される仕組みです。

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

IEnumerable<int> query = numbers.Where(n =>
{
Console.WriteLine($"判定: {n}");
return n % 2 == 0;
});

Console.WriteLine("foreachの前");

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

このコードでは、Whereを書いた時点ではまだ判定処理は実行されません。foreachで列挙したときに初めて実行されます。

Microsoft Learnでも、LINQクエリは変数を作成した時点ではなく、反復処理されたタイミングで実行されると説明されています。また、ToList()ToArray()を呼ぶと即時実行して結果をキャッシュできます。Microsoft Learn+1

3. Listとは何か

3-1. List<T>の基本的な役割

List<T>は、C#でよく使われる可変長のコレクションです。

配列は基本的にサイズが固定ですが、List<T>は要素を追加すると必要に応じて内部的に容量を増やしてくれます。そのため、要素数が変わるデータを扱うときに便利です。

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

fruits.Add("Apple");
fruits.Add("Banana");
fruits.Add("Orange");

List<T>は、内部的に配列を使い、必要に応じてサイズを増やすコレクションです。Microsoft Learnでも、List<T>ArrayListのジェネリック版であり、必要に応じて動的にサイズが増える配列を使うと説明されています。Microsoft Learn

3-2. Add・Remove・Insertなど要素を変更できる

List<T>の大きな特徴は、要素を追加・削除・挿入できることです。

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

names.Add("Alice");
names.Add("Bob");
names.Insert(1, "Charlie");
names.Remove("Bob");

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

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

C#
Alice
Charlie

Addは末尾に要素を追加し、Insertは指定した位置に要素を挿入し、Removeは指定した要素を削除します。

IEnumerable<T>にはこれらのメソッドがないため、要素を変更したい場合はList<T>を使うのが基本です。

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

List<T>は、配列のようにインデックスで要素にアクセスできます。

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

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

出力結果は次のとおりです。

C#
Alice
Bob

インデックスは0から始まります。names[0]は1番目の要素、names[1]は2番目の要素です。

Microsoft Learnでも、List<T>の要素は整数インデックスでアクセスでき、インデックスは0始まりであると説明されています。Microsoft Learn

3-4. Countで要素数をすぐ取得できる

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

C#
List<int> numbers = new List<int> { 10, 20, 30 };

Console.WriteLine(numbers.Count);

出力結果は次のとおりです。

C#
3

Countプロパティを使うと、List<T>に含まれる要素数を簡単に取得できます。

IEnumerable<T>の場合はCountプロパティがないため、LINQのCount()メソッドを使うことがあります。ただし、Count()は状況によって列挙が発生するため、使いどころに注意が必要です。

3-5. Listが向いている処理

List<T>は、次のような処理に向いています。

処理List<T>が向いている理由
要素を追加するAddが使える
要素を削除するRemoveRemoveAtが使える
途中に要素を挿入するInsertが使える
インデックスで取得するitems[0]のようにアクセスできる
要素数を頻繁に使うCountプロパティが使える
同じデータを何度も使うメモリ上に保持されている
並べ替えたいSortが使える

たとえば、ユーザーが入力した値を順番に追加していく処理や、画面表示用に一度作成したデータを何度も参照する処理ではList<T>が便利です。

4. IEnumerableとListの具体的な違い

4-1. データを保持するかどうかの違い

List<T>はデータを実際に保持します。

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

この場合、numbersの中には123というデータが保持されています。

一方、IEnumerable<T>は必ずしもデータを保持しているとは限りません。

C#
IEnumerable<int> query = numbers.Where(n => n > 1);

このqueryは「numbersから1より大きい値を取り出す処理」を表しています。LINQの遅延実行では、クエリ変数は結果そのものではなく処理内容を保持し、列挙されたときに実行されます。Microsoft Learn

そのため、IEnumerable<T>は「データの入れ物」というより「順番に取り出せるもの」と考えると理解しやすくなります。

4-2. 要素を追加・削除できるかの違い

List<T>では要素を追加・削除できます。

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

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

一方、IEnumerable<T>では次のようにAddは使えません。

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

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

実体がList<string>であっても、変数の型がIEnumerable<string>である限り、IEnumerable<T>に定義されている操作しか使えません。

追加や削除をしたい場合は、最初からList<T>として扱うか、ToList()で変換します。

C#
IEnumerable<string> names = GetNames();

List<string> list = names.ToList();
list.Add("Alice");

4-3. インデックスアクセスできるかの違い

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

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

Console.WriteLine(names[0]);

しかし、IEnumerable<T>は基本的にインデックスアクセスできません。

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

// Console.WriteLine(names[0]); // コンパイルエラー

IEnumerable<T>で特定の位置の要素を取り出したい場合は、LINQのElementAt()を使うこともできます。

C#
Console.WriteLine(names.ElementAt(0));

ただし、ElementAt()はコレクションの種類によっては先頭から順番にたどる場合があります。頻繁にインデックスアクセスするなら、List<T>や配列を使う方が自然です。

4-4. CountとCount()の違い

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

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

int count = numbers.Count;

一方、IEnumerable<T>では一般的にLINQのCount()メソッドを使います。

C#
IEnumerable<int> numbers = GetNumbers();

int count = numbers.Count();

見た目は似ていますが、意味は少し違います。

List<T>.Countは、リストが保持している要素数を返すプロパティです。Microsoft Learnでも、CountList<T>に含まれる要素数を取得するプロパティとして説明されています。Microsoft Learn

一方、IEnumerable<T>.Count()はLINQの拡張メソッドです。対象によっては要素を最初から最後まで列挙して数える可能性があります。

そのため、次のようにCount()を何度も呼ぶコードは注意が必要です。

C#
IEnumerable<int> numbers = GetNumbers();

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

この場合、GetNumbers()の中身によっては複数回列挙される可能性があります。空かどうかを知りたいだけなら、Any()を使う方が適しています。

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

4-5. 遅延実行と即時実行の違い

IEnumerable<T>を返すLINQの多くは遅延実行です。

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

IEnumerable<int> query = numbers.Where(n => n >= 3);

この時点では、まだWhereの条件判定は実行されていません。次のようにforeachで列挙したときに実行されます。

C#
foreach (int number in query)
{
Console.WriteLine(number);
}

一方、ToList()を呼ぶと、その時点で処理が実行され、結果がList<T>としてメモリ上に確定します。

C#
List<int> list = numbers.Where(n => n >= 3).ToList();

Microsoft Learnでも、Whereは遅延実行であり、foreachGetEnumerator()などで列挙されるまでクエリは実行されないと説明されています。Microsoft Learn

4-6. パフォーマンス面での違い

IEnumerable<T>List<T>のどちらが速いかは、処理内容によって変わります。

IEnumerable<T>は、必要なタイミングで必要な分だけ処理できるため、大量データを順番に処理する場合に効率的なことがあります。特にLINQの遅延実行では、条件に合う要素を必要な分だけ取り出せる場合があります。

一方、List<T>はメモリ上にデータを保持するため、同じデータを何度も使う場合や、インデックスアクセスが必要な場合に向いています。

注意したいのは、ToList()を使うとすべての結果をメモリ上に保持することです。データ件数が多い場合、不要なToList()はメモリ使用量を増やします。Microsoft Learnでも、クエリ途中でのコレクションの実体化は、メモリやパフォーマンス特性を大きく変える可能性があると説明されています。Microsoft Learn

5. IEnumerableとListの使い分け

5-1. メソッドの戻り値にIEnumerableを使うケース

メソッドの戻り値では、IEnumerable<T>を使うと柔軟な設計になります。

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

return names.Where(name => name.StartsWith("A"));
}

このメソッドの利用者は、戻り値をforeachで処理できます。

C#
foreach (string name in GetActiveUserNames())
{
Console.WriteLine(name);
}

戻り値をIEnumerable<T>にしておくと、内部実装をList<T>から配列やLINQクエリに変えても、呼び出し側への影響を小さくできます。

ただし、呼び出し側に「追加・削除してほしい」「インデックスでアクセスしてほしい」という意図があるなら、List<T>IReadOnlyList<T>など別の型を検討します。

5-2. メソッドの引数にIEnumerableを使うケース

メソッドの引数には、IEnumerable<T>を使うと受け取れる型の幅が広がります。

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

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

C#
PrintItems(new List<string> { "A", "B" });
PrintItems(new string[] { "X", "Y" });

メソッド内で要素を列挙するだけなら、引数はList<T>ではなくIEnumerable<T>にする方が柔軟です。

悪い例は次のようなコードです。

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

このコードでも動きますが、引数がList<string>に限定されます。配列や他のコレクションを渡したい場合に不便です。

5-3. 要素の追加・削除が必要ならListを使う

メソッド内で要素を追加・削除するなら、List<T>を使うのがわかりやすいです。

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

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

return names;
}

また、引数として受け取ったコレクションに要素を追加したい場合も、List<T>ICollection<T>など、変更可能な型を使う必要があります。

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

IEnumerable<T>は列挙用の型なので、要素変更の意図がある場合には適していません。

5-4. 複数回ループするならList化を検討する

IEnumerable<T>は複数回列挙すると、そのたびに処理が実行される場合があります。

C#
IEnumerable<int> query = GetNumbers().Where(n => n > 10);

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

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

このコードでは、queryが遅延実行の場合、2回列挙されます。

処理が軽ければ問題ありませんが、データベースアクセスや重い計算を含む場合は非効率になることがあります。

そのような場合は、最初にToList()で結果を確定させるとよいです。

C#
List<int> numbers = GetNumbers()
.Where(n => n > 10)
.ToList();

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

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

ToList()により一度だけ列挙して結果を保持できるため、同じデータを複数回使う処理に向いています。

5-5. API設計ではIEnumerableとListのどちらを選ぶべきか

API設計では、目的に応じて型を選ぶことが大切です。

基本的な考え方は次のとおりです。

場面おすすめ
引数で「列挙できればよい」IEnumerable<T>
戻り値で「読み取り用の一覧」を返すIEnumerable<T>
戻り値で「件数やインデックスアクセス」を期待させるIReadOnlyList<T>
内部で追加・削除するList<T>
呼び出し側にも変更させたいList<T>またはICollection<T>
結果を確定して返したいList<T>またはIReadOnlyList<T>

実務では、公開メソッドの引数や戻り値はできるだけ抽象的な型にし、内部実装では必要に応じてList<T>を使うことが多いです。

たとえば、次のような設計はよく使われます。

C#
public IEnumerable<Product> GetProducts()
{
return _products.Where(product => product.IsActive);
}

一方、呼び出し側がインデックスアクセスや件数取得を頻繁に行う前提なら、IReadOnlyList<T>List<T>を返すことも検討します。

6. IEnumerableとListの変換方法

6-1. ListからIEnumerableへ変換する方法

List<T>IEnumerable<T>を実装しているため、特別な変換処理を書かなくてもIEnumerable<T>として扱えます。

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

IEnumerable<string> enumerable = list;

この代入はそのまま可能です。

また、メソッドの引数がIEnumerable<T>の場合も、List<T>をそのまま渡せます。

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

List<string> list = new List<string> { "Alice", "Bob" };

PrintNames(list);

このように、List<T>からIEnumerable<T>への変換は自然に行えます。

6-2. IEnumerableからListへToList()で変換する方法

IEnumerable<T>List<T>に変換するには、LINQのToList()を使います。

C#
IEnumerable<string> names = GetNames();

List<string> list = names.ToList();

ToList()を使うには、通常はSystem.Linqが必要です。

C#
using System.Linq;

ToList()を呼ぶと、その時点でIEnumerable<T>が列挙され、結果がList<T>としてメモリ上に保持されます。

たとえば、次のようにLINQの結果をリスト化できます。

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

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

この場合、evenNumbersには24が入ります。

Microsoft Learnでも、ToList()ToArray()を呼び出すとクエリを即時実行し、結果をキャッシュできると説明されています。Microsoft Learn

6-3. ToArray()との違い

IEnumerable<T>を具体的なコレクションに変換する方法として、ToList()のほかにToArray()があります。

C#
IEnumerable<int> numbers = GetNumbers();

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

ToList()List<T>を返し、ToArray()は配列を返します。

違いは次のとおりです。

メソッド戻り値向いている用途
ToList()List<T>後から追加・削除したい
ToArray()T[]固定長のデータとして扱いたい
ToList()可変長要素変更に向いている
ToArray()固定長シンプルなデータ受け渡しに向いている

後から要素を追加・削除したいならToList()、固定された結果として扱いたいならToArray()を選ぶとよいでしょう。

ただし、どちらも即時実行であり、結果をメモリ上に保持します。大量データを扱う場合は、必要なタイミングで使うことが大切です。

6-4. Cast・Select・Whereを使った変換例

IEnumerable<T>はLINQと組み合わせて、型変換や絞り込みに使えます。

まず、Whereで条件に合う要素だけを取り出す例です。

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

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

次に、Selectで別の型に変換する例です。

C#
List<User> users = new List<User>
{
new User { Id = 1, Name = "Alice" },
new User { Id = 2, Name = "Bob" }
};

IEnumerable<string> names = users.Select(user => user.Name);

Selectを使うと、Userの一覧から名前だけの一覧を作れます。

Cast<T>()は、非ジェネリックなコレクションやobjectの列挙を特定の型として扱いたい場合に使います。

C#
ArrayList arrayList = new ArrayList
{
"Alice",
"Bob"
};

IEnumerable<string> names = arrayList.Cast<string>();

ただし、Cast<T>()は実行時に変換できない要素があると例外が発生します。安全に変換したい場合はOfType<T>()を使う選択肢もあります。

C#
ArrayList values = new ArrayList
{
"Alice",
123,
"Bob"
};

IEnumerable<string> names = values.OfType<string>();

この場合、文字列だけが取り出されます。

6-5. 変換時に注意すべきメモリ使用量

ToList()ToArray()は便利ですが、使いすぎには注意が必要です。

C#
List<User> users = GetUsers()
.Where(user => user.IsActive)
.ToList();

このコードでは、条件に合うすべてのユーザーをメモリ上に保持します。データ件数が少ない場合は問題になりにくいですが、数十万件、数百万件のような大量データではメモリ使用量が増える可能性があります。

また、次のようにLINQの途中で不用意にToList()を挟むと、本来なら遅延実行で効率よく処理できたものが、途中で全件メモリに読み込まれることがあります。

C#
var result = GetUsers()
.ToList()
.Where(user => user.IsActive)
.Select(user => user.Name);

この場合、先に全ユーザーをリスト化してから絞り込んでいます。

多くの場合は、絞り込みや変換を先に行い、最後に必要であればToList()します。

C#
var result = GetUsers()
.Where(user => user.IsActive)
.Select(user => user.Name)
.ToList();

不要なタイミングでの実体化は、メモリ使用量やパフォーマンスに影響します。Microsoft Learnでも、クエリの途中でコレクションを実体化すると、メモリやパフォーマンスの特性が大きく変わる場合があると説明されています。Microsoft Learn

7. IEnumerableとListのコード例

7-1. IEnumerableを使った基本コード

まずは、IEnumerable<T>を使った基本的なコードです。

C#
using System;
using System.Collections.Generic;

class Program
{
static void Main()
{
IEnumerable<string> names = GetNames();

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

static IEnumerable<string> GetNames()
{
return new List<string>
{
"Alice",
"Bob",
"Charlie"
};
}
}

このコードでは、GetNames()の戻り値をIEnumerable<string>にしています。

呼び出し側は、戻ってきたデータがList<string>なのか配列なのかを気にせず、foreachで処理できます。

7-2. Listを使った基本コード

次に、List<T>を使った基本コードです。

C#
using System;
using System.Collections.Generic;

class Program
{
static void Main()
{
List<string> names = new List<string>();

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

names.Remove("Bob");

Console.WriteLine(names[0]);
Console.WriteLine($"Count: {names.Count}");

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

このコードでは、Addで要素を追加し、Removeで要素を削除し、names[0]でインデックスアクセスしています。

このように、要素を変更したり、位置を指定して取り出したりする場合はList<T>が適しています。

7-3. LINQのWhereでIEnumerableを返す例

LINQのWhereは、条件に合う要素を返します。

C#
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
static void Main()
{
List<int> numbers = new List<int>
{
1, 2, 3, 4, 5, 6
};

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

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

出力結果は次のとおりです。

C#
2
4
6

Whereの戻り値はIEnumerable<int>です。これは、条件に合う要素を順番に取り出せることを表しています。

また、Whereは遅延実行です。Microsoft Learnでも、Whereは遅延実行で実装され、列挙されるまでクエリは実行されないと説明されています。Microsoft Learn

7-4. ToList()で即時実行する例

LINQの結果をすぐに確定させたい場合は、ToList()を使います。

C#
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
static void Main()
{
List<int> numbers = new List<int>
{
1, 2, 3, 4, 5, 6
};

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

evenNumbers.Add(8);

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

出力結果は次のとおりです。

C#
2
4
6
8

ToList()List<int>に変換しているため、後からAddで要素を追加できます。

7-5. foreachで処理する例

IEnumerable<T>List<T>も、どちらもforeachで処理できます。

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

foreach (string name in enumerableNames)
{
Console.WriteLine(name);
}
C#
List<string> listNames = new List<string>
{
"Alice",
"Bob"
};

foreach (string name in listNames)
{
Console.WriteLine(name);
}

foreachで順番に処理するだけなら、IEnumerable<T>で十分です。

一方、ループの前後で要素を追加・削除したい場合や、インデックスでアクセスしたい場合はList<T>を使います。

8. IEnumerableとListでよくある注意点

8-1. IEnumerableを何度も列挙すると処理が重くなる場合がある

IEnumerable<T>は、何度も列挙するとそのたびに処理が実行される場合があります。

C#
IEnumerable<int> query = GetNumbers().Where(n => n > 100);

int count = query.Count();

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

このコードでは、Count()で一度列挙し、foreachでもう一度列挙する可能性があります。

GetNumbers()が単純なリストを返すだけなら大きな問題にならないこともありますが、データベースアクセス、ファイル読み込み、API呼び出し、重い計算が含まれる場合は注意が必要です。

複数回使うなら、次のようにToList()で確定させることを検討します。

C#
List<int> numbers = GetNumbers()
.Where(n => n > 100)
.ToList();

int count = numbers.Count;

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

8-2. 遅延実行による予期しない結果

遅延実行では、クエリを作った後に元のコレクションを変更すると、列挙時の状態が結果に反映されることがあります。

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

IEnumerable<int> query = numbers.Where(n => n >= 2);

numbers.Add(4);

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

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

C#
2
3
4

queryを作った時点では4は存在しませんでした。しかし、foreachで列挙する前にnumbers.Add(4)を実行しているため、結果に4が含まれます。

クエリ作成時点の結果を固定したい場合は、ToList()を使います。

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

numbers.Add(4);

foreach (int number in result)
{
Console.WriteLine(number);
}

この場合、resultにはToList()を実行した時点の結果が保持されます。

8-3. nullチェックと空コレクションの扱い

IEnumerable<T>List<T>を扱うときは、nullと空コレクションを区別することが重要です。

C#
IEnumerable<string>? names = null;

この状態でforeachすると、NullReferenceExceptionが発生します。

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

安全に扱うには、nullチェックを行います。

C#
if (names != null)
{
foreach (string name in names)
{
Console.WriteLine(name);
}
}

また、メソッドの戻り値では、可能であればnullではなく空のコレクションを返す方が扱いやすいです。

C#
IEnumerable<string> GetNames()
{
return Enumerable.Empty<string>();
}

空のコレクションを返せば、呼び出し側はnullチェックなしでforeachできます。

C#
foreach (string name in GetNames())
{
Console.WriteLine(name);
}

8-4. Count()を多用すると非効率になる場合がある

IEnumerable<T>に対してCount()を何度も使うと、非効率になる場合があります。

C#
IEnumerable<int> numbers = GetNumbers();

if (numbers.Count() > 0)
{
Console.WriteLine($"件数: {numbers.Count()}");
}

このコードでは、Count()を2回呼んでいます。対象が単純なコレクションなら問題が小さいこともありますが、遅延実行のクエリでは複数回列挙される可能性があります。

空かどうかを知りたいだけなら、Any()を使います。

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

件数を何度も使うなら、ToList()で確定させてからCountプロパティを使うとわかりやすいです。

C#
List<int> list = numbers.ToList();

if (list.Count > 0)
{
Console.WriteLine($"件数: {list.Count}");
}

8-5. IQueryableとの違いにも注意する

IEnumerable<T>と似た型にIQueryable<T>があります。

IEnumerable<T>は主にメモリ上のコレクションを列挙する場面で使われます。一方、IQueryable<T>はデータベースなど外部データソースへの問い合わせでよく使われます。

Entity Frameworkなどでは、IQueryable<T>に対するWhereSelectがSQLに変換されることがあります。

C#
IQueryable<User> query = dbContext.Users
.Where(user => user.IsActive);

この段階では、まだデータベースに問い合わせていないことが多く、ToList()などで列挙したタイミングでSQLが実行されます。

C#
List<User> users = query.ToList();

注意したいのは、早い段階でAsEnumerable()ToList()を呼ぶと、それ以降の処理がメモリ上で行われる可能性があることです。

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

このコードでは、先に全件をメモリに読み込んでから絞り込む可能性があります。データベースを使う場合は、できるだけWhereSelectで絞り込んでからToList()するのが基本です。

9. IEnumerableとListに関するよくある質問

9-1. IEnumerableとListはどちらを使うべき?

目的によって使い分けます。

順番に読み取るだけならIEnumerable<T>で十分です。メソッドの引数や戻り値で「列挙できればよい」場合も、IEnumerable<T>を使うと柔軟な設計になります。

一方、要素を追加・削除したい、インデックスでアクセスしたい、件数を頻繁に使いたい、同じ結果を何度も使いたい場合はList<T>が向いています。

簡単にまとめると、次のようになります。

判断基準選ぶ型
読むだけIEnumerable<T>
foreachで処理するだけIEnumerable<T>
LINQの結果を扱うIEnumerable<T>
追加・削除するList<T>
インデックスでアクセスするList<T>
結果をメモリに固定するList<T>

迷った場合は、外部に見せる型はIEnumerable<T>、内部で操作する型はList<T>と考えると整理しやすいです。

9-2. IEnumerableにAddできないのはなぜ?

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

AddRemoveは、コレクションの内容を変更する操作です。しかし、IEnumerable<T>は読み取りや列挙のための最小限の機能しか持っていません。

たとえば、次のコードはコンパイルエラーになります。

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

// names.Add("Alice"); // エラー

実体がList<string>であっても、変数の型がIEnumerable<string>なので、IEnumerable<T>に定義されていないAddは呼び出せません。

追加したい場合は、List<T>として扱います。

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

names.Add("Alice");

または、IEnumerable<T>ToList()で変換します。

C#
IEnumerable<string> names = GetNames();

List<string> list = names.ToList();
list.Add("Alice");

9-3. ListをIEnumerableで受け取るメリットは?

List<T>IEnumerable<T>で受け取るメリットは、メソッドの柔軟性が上がることです。

次のように引数をIEnumerable<T>にすると、List<T>だけでなく、配列やLINQの結果も渡せます。

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

呼び出し例は次のとおりです。

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

PrintNames(new string[] { "Tom", "Jerry" });

PrintNames(GetNames().Where(name => name.StartsWith("A")));

もし引数をList<string>にしてしまうと、基本的にはList<string>しか受け取れません。

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

メソッド内で列挙するだけなら、List<T>ではなくIEnumerable<T>で受け取る方が使いやすい設計になります。

9-4. ToList()はいつ使うべき?

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

具体的には、次のような場面です。

場面ToList()を使う理由
同じ結果を複数回ループする再列挙を避ける
後からAddやRemoveしたいList<T>に変換する必要がある
LINQの結果を固定したい遅延実行の影響を避ける
データベース問い合わせを実行したい結果をメモリに読み込む
Countを何度も使いたいCountプロパティを使える

たとえば、次のようなケースではToList()が有効です。

C#
List<User> activeUsers = users
.Where(user => user.IsActive)
.ToList();

Console.WriteLine(activeUsers.Count);

foreach (User user in activeUsers)
{
Console.WriteLine(user.Name);
}

ただし、不要なToList()は避けるべきです。

C#
var names = users
.ToList()
.Where(user => user.IsActive)
.Select(user => user.Name);

このように早い段階でToList()すると、必要以上にメモリを使う可能性があります。基本的には、絞り込みや変換をした後、必要なタイミングでToList()します。

9-5. 配列・ICollection・IReadOnlyListとの違いは?

IEnumerable<T>List<T>のほかにも、C#には複数のコレクション関連型があります。

代表的な違いは次のとおりです。

特徴向いている用途
IEnumerable<T>順番に列挙できるforeach、LINQ、読み取り
List<T>追加・削除・インデックスアクセスができる可変の一覧
配列 T[]固定長、インデックスアクセス可能要素数が固定のデータ
ICollection<T>Count、Add、Removeなどを持つ変更可能なコレクションの抽象化
IReadOnlyList<T>読み取り専用のリスト形式件数とインデックスアクセスを提供したい
IReadOnlyCollection<T>読み取り専用でCountを持つ件数を公開したいが変更させたくない

たとえば、単にforeachできればよいならIEnumerable<T>で十分です。

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

件数とインデックスアクセスを公開したいが、呼び出し側に変更させたくないならIReadOnlyList<T>が向いています。

C#
IReadOnlyList<string> GetNames()
{
return new List<string>
{
"Alice",
"Bob"
};
}

追加・削除も含めて扱いたいならList<T>ICollection<T>を検討します。

C#
void AddItem(ICollection<string> items)
{
items.Add("New Item");
}

このように、必要な機能に応じて型を選ぶことが大切です。

まとめ

C#のIEnumerable<T>List<T>は、どちらも複数のデータを扱う場面でよく使われますが、役割は異なります。

IEnumerable<T>は「順番に取り出せるもの」を表すインターフェイスです。foreachで列挙したり、LINQの結果を扱ったり、メソッドの引数や戻り値を柔軟にしたりする場面に向いています。

一方、List<T>は「要素を保持して操作できるコレクション」です。AddRemoveInsert、インデックスアクセス、Countプロパティなどが使えるため、データを変更したい場合や、同じ結果を何度も利用したい場合に向いています。

使い分けの基本は次のとおりです。

目的選ぶ型
順番に読むだけIEnumerable<T>
LINQで絞り込み・変換するIEnumerable<T>
メソッドの引数を柔軟にしたいIEnumerable<T>
要素を追加・削除したいList<T>
インデックスでアクセスしたいList<T>
結果を確定して再利用したいToList()List<T>
大量データを効率よく処理したい不要なToList()を避ける

特に重要なのは、IEnumerable<T>には遅延実行が関係する場合があることです。LINQのWhereなどは、クエリを作った時点ではなく、foreachToList()などで列挙されたタイミングで実行されます。遅延実行を理解しておくと、予期しない結果やパフォーマンスの問題を避けやすくなります。

迷ったときは、「読み取り・列挙ならIEnumerable<T>」「変更・保持・インデックスアクセスならList<T>」と覚えておくとよいでしょう。