C#のIEnumerableとは?Listとの違い・使い方・LINQ活用まで初心者向けに徹底解説
はじめに
C#で配列やListを扱っていると、IEnumerableという型をよく見かけます。
たとえば、次のようなコードです。
C#IEnumerable<string> names = new List<string>
{
"田中",
"佐藤",
"鈴木"
};
初心者のうちは「Listと何が違うの?」「なぜわざわざIEnumerableを使うの?」「LINQとどう関係するの?」と疑問に感じるかもしれません。
IEnumerableは、C#でコレクションを扱ううえで非常に重要な考え方です。配列、List、Dictionary、LINQ、Entity Frameworkなど、さまざまな場面で登場します。
この記事では、csharp ienumerableについて初心者にもわかりやすく、Listとの違い、基本的な使い方、LINQとの関係、遅延実行、実務での使い分けまで詳しく解説します。
1. C#のIEnumerableとは?まずは初心者向けにわかりやすく理解しよう
1-1. IEnumerableは「順番に取り出せるデータ」を表すインターフェース
C#のIEnumerableとは、簡単にいうと「要素を順番に取り出せるもの」を表すインターフェースです。
インターフェースとは、「こういう機能を持っています」という約束のようなものです。IEnumerableを実装している型は、foreachで1つずつ要素を取り出せます。
たとえば、次のようなものはすべてIEnumerableとして扱えます。
C#string[] array = { "A", "B", "C" };
List<string> list = new List<string>
{
"A",
"B",
"C"
};
IEnumerable<string> enumerable = list;
IEnumerable<string>は、「string型の要素を順番に取り出せるデータ」という意味です。
つまり、IEnumerableはデータそのものを表すというより、「データを順番に列挙できる性質」を表します。
1-2. 配列・List・DictionaryなどでIEnumerableが使われる理由
C#では、配列、List、Dictionaryなど多くのコレクションがIEnumerableを実装しています。
C#int[] numbersArray = { 1, 2, 3 };
List<int> numbersList = new List<int>
{
1,
2,
3
};
Dictionary<string, int> scores = new Dictionary<string, int>
{
{ "田中", 80 },
{ "佐藤", 90 }
};
これらは型としては別物ですが、どれも「順番に取り出す」ことができます。
C#foreach (int number in numbersArray)
{
Console.WriteLine(number);
}
foreach (int number in numbersList)
{
Console.WriteLine(number);
}
foreach (KeyValuePair<string, int> score in scores)
{
Console.WriteLine($"{score.Key}: {score.Value}");
}
配列もListもDictionaryも、内部構造は異なります。しかし、IEnumerableを使えば「順番に取り出せるもの」として同じように扱えます。
これにより、メソッドの引数や戻り値を柔軟に設計できます。
1-3. IEnumerableとIEnumerable<T>の違い
C#には、IEnumerableとIEnumerable<T>があります。
IEnumerableは非ジェネリック版で、要素の型が明確ではありません。
C#IEnumerable items;
一方、IEnumerable<T>はジェネリック版で、要素の型を指定できます。
C#IEnumerable<string> names;
IEnumerable<int> numbers;
現在のC#では、基本的にIEnumerable<T>を使うのが一般的です。
理由は、型安全だからです。
C#IEnumerable<string> names = new List<string>
{
"田中",
"佐藤"
};
foreach (string name in names)
{
Console.WriteLine(name);
}
IEnumerable<string>であれば、要素がstringであることがコンパイル時にわかります。そのため、不要なキャストを避けられ、コードも読みやすくなります。
特別な理由がなければ、初心者はIEnumerable<T>を使うと覚えておけば問題ありません。
1-4. foreachで使える理由とGetEnumeratorの基本
IEnumerableを実装している型は、foreachで使えます。
C#IEnumerable<int> numbers = new List<int>
{
1,
2,
3
};
foreach (int number in numbers)
{
Console.WriteLine(number);
}
なぜforeachで使えるのかというと、IEnumerableにはGetEnumeratorというメソッドが定義されているからです。
GetEnumeratorは、要素を順番に取り出すための列挙子を返します。
イメージとしては、次のような流れです。
C#IEnumerator<int> enumerator = numbers.GetEnumerator();
while (enumerator.MoveNext())
{
int number = enumerator.Current;
Console.WriteLine(number);
}
通常、このように自分でGetEnumeratorを書くことはあまりありません。foreachを使えば、C#が内部的にこの処理を行ってくれます。
つまり、IEnumerableはforeachで回せる仕組みの土台になっています。
1-5. IEnumerableを理解するとLINQがわかりやすくなる
IEnumerableを理解すると、LINQもかなり理解しやすくなります。
LINQとは、コレクションに対して絞り込み、変換、並び替えなどを簡潔に書ける機能です。
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);
}
この例では、Whereによって偶数だけを取り出しています。
LINQの多くのメソッドは、IEnumerable<T>に対して使えます。そして、LINQの結果も多くの場合IEnumerable<T>になります。
そのため、C#でLINQを使いこなすには、IEnumerableの理解が欠かせません。
2. IEnumerableの基本的な使い方
2-1. IEnumerable型の変数を宣言する方法
IEnumerable型の変数は、次のように宣言します。
C#IEnumerable<int> numbers;
この時点では、まだ中身はありません。
実際には、Listや配列などを代入して使います。
C#IEnumerable<int> numbers = new List<int>
{
1,
2,
3
};
配列も代入できます。
C#IEnumerable<string> names = new string[]
{
"田中",
"佐藤",
"鈴木"
};
IEnumerable<T>のTには、要素の型を指定します。
C#IEnumerable<int> numbers;
IEnumerable<string> names;
IEnumerable<User> users;
IEnumerable<User>であれば、「User型のデータを順番に取り出せるもの」という意味になります。
2-2. ListをIEnumerableとして扱うサンプルコード
ListはIEnumerable<T>を実装しているため、ListをIEnumerableとして扱えます。
C#List<string> nameList = new List<string>
{
"田中",
"佐藤",
"鈴木"
};
IEnumerable<string> names = nameList;
このように代入できます。
ただし、namesはIEnumerable<string>型なので、Listが持っているAddやRemoveなどのメソッドは直接使えません。
C#// names.Add("高橋"); // コンパイルエラー
実体はListですが、変数の型がIEnumerable<string>であるため、IEnumerableとして見える機能しか使えないのです。
この考え方は、C#の型を理解するうえで重要です。
2-3. foreachで要素を1つずつ取り出す方法
IEnumerableは、foreachで要素を1つずつ取り出せます。
C#IEnumerable<int> numbers = new List<int>
{
10,
20,
30
};
foreach (int number in numbers)
{
Console.WriteLine(number);
}
実行結果は次のようになります。
C#10
20
30
foreachを使うことで、コレクションの中身を先頭から順番に処理できます。
配列でもListでも、IEnumerableとして扱えば同じ書き方で処理できます。
C#void PrintNames(IEnumerable<string> names)
{
foreach (string name in names)
{
Console.WriteLine(name);
}
}
このメソッドは、Listでも配列でも受け取れます。
C#PrintNames(new List<string> { "田中", "佐藤" });
PrintNames(new string[] { "鈴木", "高橋" });
2-4. メソッドの戻り値にIEnumerableを使う例
メソッドの戻り値としてIEnumerable<T>を使うこともよくあります。
C#IEnumerable<string> GetNames()
{
return new List<string>
{
"田中",
"佐藤",
"鈴木"
};
}
呼び出し側では、foreachで処理できます。
C#IEnumerable<string> names = GetNames();
foreach (string name in names)
{
Console.WriteLine(name);
}
戻り値をIEnumerable<T>にすると、呼び出し側は「順番に取り出せるデータ」として扱えます。
この場合、メソッド内部でListを使っているか配列を使っているかを呼び出し側に意識させずに済みます。
C#IEnumerable<int> GetEvenNumbers()
{
List<int> numbers = new List<int>
{
1,
2,
3,
4,
5
};
return numbers.Where(n => n % 2 == 0);
}
このように、LINQの結果をそのままIEnumerable<T>として返すこともできます。
2-5. メソッドの引数にIEnumerableを使う例
メソッドの引数にIEnumerable<T>を使うと、さまざまなコレクションを受け取れる柔軟なメソッドになります。
C#void PrintNumbers(IEnumerable<int> numbers)
{
foreach (int number in numbers)
{
Console.WriteLine(number);
}
}
このメソッドには、Listも配列も渡せます。
C#List<int> list = new List<int> { 1, 2, 3 };
int[] array = { 4, 5, 6 };
PrintNumbers(list);
PrintNumbers(array);
引数をList<int>にしてしまうと、Listしか渡せません。
C#void PrintNumbers(List<int> numbers)
{
foreach (int number in numbers)
{
Console.WriteLine(number);
}
}
この場合、配列をそのまま渡すことはできません。
そのため、メソッド内で要素を順番に読むだけなら、引数はIEnumerable<T>にするのが便利です。
2-6. IEnumerableはAddやRemoveができない点に注意
IEnumerable<T>は、要素を順番に取り出すための型です。
そのため、AddやRemoveのようにコレクションを変更するメソッドはありません。
C#IEnumerable<string> names = new List<string>
{
"田中",
"佐藤"
};
// names.Add("鈴木"); // エラー
// names.Remove("田中"); // エラー
追加や削除をしたい場合は、List<T>などの変更可能なコレクションを使います。
C#List<string> names = new List<string>
{
"田中",
"佐藤"
};
names.Add("鈴木");
names.Remove("田中");
IEnumerable<T>は「読み取り・列挙向き」、List<T>は「追加・削除・更新向き」と考えるとわかりやすいです。
3. IEnumerableとListの違い
3-1. IEnumerableはインターフェース、Listは具体的なクラス
IEnumerable<T>とList<T>の大きな違いは、IEnumerable<T>がインターフェースで、List<T>が具体的なクラスである点です。
C#IEnumerable<int> numbers1;
List<int> numbers2;
IEnumerable<int>は、「int型の要素を順番に取り出せるもの」という約束を表します。
一方、List<int>は、実際にデータを保持できる具体的なコレクションです。
C#List<int> list = new List<int>
{
1,
2,
3
};
IEnumerable<int> enumerable = list;
ListはIEnumerableの機能を持っているため、IEnumerable型の変数に代入できます。
ただし、IEnumerableとして扱うと、使える機能は列挙に関するものに限られます。
3-2. IEnumerableは読み取り・列挙向き、Listは追加・削除・更新向き
IEnumerable<T>は、基本的に要素を順番に読み取るために使います。
C#IEnumerable<string> names = new List<string>
{
"田中",
"佐藤"
};
foreach (string name in names)
{
Console.WriteLine(name);
}
一方、List<T>は、要素の追加、削除、更新に向いています。
C#List<string> names = new List<string>
{
"田中",
"佐藤"
};
names.Add("鈴木");
names.Remove("田中");
names[0] = "高橋";
読み取るだけならIEnumerable<T>で十分です。
コレクションを変更したい場合は、List<T>を使う必要があります。
3-3. IEnumerableはインデックスアクセスできない
Listでは、インデックスを使って要素にアクセスできます。
C#List<string> names = new List<string>
{
"田中",
"佐藤",
"鈴木"
};
Console.WriteLine(names[0]);
しかし、IEnumerable<T>ではインデックスアクセスはできません。
C#IEnumerable<string> names = new List<string>
{
"田中",
"佐藤",
"鈴木"
};
// Console.WriteLine(names[0]); // エラー
IEnumerable<T>は、あくまで「順番に取り出せる」ことだけを保証しています。
特定の位置の要素を直接取り出したい場合は、ToList()でListに変換するか、LINQのElementAtを使います。
C#string firstName = names.ElementAt(0);
ただし、ElementAtは対象によっては先頭から順番に数えるため、Listのインデックスアクセスより効率が悪くなる場合があります。
3-4. ListはCountやAddなど便利な機能を持つ
List<T>には、便利なプロパティやメソッドが多く用意されています。
C#List<int> numbers = new List<int>
{
1,
2,
3
};
Console.WriteLine(numbers.Count);
numbers.Add(4);
numbers.Remove(2);
Console.WriteLine(numbers[0]);
主な機能には、次のようなものがあります。
| 機能 | 説明 |
|---|---|
Count | 要素数を取得する |
Add | 要素を追加する |
Remove | 要素を削除する |
Clear | すべての要素を削除する |
Contains | 指定した要素が含まれるか確認する |
[index] | 指定した位置の要素にアクセスする |
一方、IEnumerable<T>は非常にシンプルです。
基本的には、foreachなどで順番に取り出すために使います。
3-5. IEnumerableとListの違いを比較表で整理
IEnumerable<T>とList<T>の違いを整理すると、次のようになります。
| 項目 | IEnumerable<T> | List<T> |
|---|---|---|
| 種類 | インターフェース | 具体的なクラス |
| 主な用途 | 要素を順番に読み取る | 要素を保持・追加・削除・更新する |
| foreach | 使える | 使える |
| Add | 使えない | 使える |
| Remove | 使えない | 使える |
| インデックスアクセス | 基本的にできない | できる |
| Count | LINQのCount()で取得 | Countプロパティで取得 |
| LINQ | 使いやすい | 使いやすい |
| 柔軟性 | 高い | Listに限定される |
| 実体を持つか | 実体ではなく抽象的な型 | 実体を持つ |
簡単にいうと、IEnumerable<T>は「読むための型」、List<T>は「管理するための型」です。
3-6. どちらを使うべきか迷ったときの判断基準
IEnumerable<T>とList<T>のどちらを使うべきか迷った場合は、次の基準で考えるとよいです。
メソッド内で要素を順番に読むだけなら、IEnumerable<T>が向いています。
C#void PrintUsers(IEnumerable<User> users)
{
foreach (User user in users)
{
Console.WriteLine(user.Name);
}
}
要素を追加・削除・更新したいなら、List<T>が向いています。
C#void AddUser(List<User> users, User user)
{
users.Add(user);
}
戻り値として外部に公開する場合は、呼び出し側に変更させたくないならIEnumerable<T>、Listとして操作してほしいならList<T>を返します。
C#IEnumerable<User> GetUsers()
{
return users;
}
実務では、「必要な機能だけを公開する」という考え方が大切です。
読み取りだけでよいなら、IEnumerable<T>を使うことで余計な操作を防ぎやすくなります。
4. IEnumerableとListの変換方法
4-1. ListからIEnumerableへ変換する方法
ListはIEnumerable<T>を実装しているため、特別な変換処理を書かなくても代入できます。
C#List<int> list = new List<int>
{
1,
2,
3
};
IEnumerable<int> enumerable = list;
これは暗黙的に変換できます。
メソッドの引数がIEnumerable<T>の場合も、Listをそのまま渡せます。
C#void PrintNumbers(IEnumerable<int> numbers)
{
foreach (int number in numbers)
{
Console.WriteLine(number);
}
}
List<int> list = new List<int>
{
1,
2,
3
};
PrintNumbers(list);
配列も同じように渡せます。
C#int[] array = { 4, 5, 6 };
PrintNumbers(array);
この柔軟さが、IEnumerable<T>を使う大きなメリットです。
4-2. IEnumerableからListへ変換するToListの使い方
IEnumerable<T>をList<T>に変換したい場合は、LINQのToList()を使います。
C#IEnumerable<int> numbers = new int[]
{
1,
2,
3
};
List<int> numberList = numbers.ToList();
ToList()を使うには、通常は次のusingが必要です。
C#using System.Linq;
LINQで絞り込んだ結果をListにしたい場合にも使います。
C#List<int> numbers = new List<int>
{
1,
2,
3,
4,
5
};
List<int> evenNumbers = numbers
.Where(n => n % 2 == 0)
.ToList();
Whereの結果はIEnumerable<int>ですが、最後にToList()を呼ぶことでList<int>になります。
4-3. ToArrayで配列に変換する方法
IEnumerable<T>を配列に変換したい場合は、ToArray()を使います。
C#IEnumerable<string> names = new List<string>
{
"田中",
"佐藤",
"鈴木"
};
string[] nameArray = names.ToArray();
LINQの結果を配列にすることもできます。
C#int[] evenNumbers = new List<int>
{
1,
2,
3,
4,
5
}
.Where(n => n % 2 == 0)
.ToArray();
Listとして追加・削除したい場合はToList()、固定的な配列として扱いたい場合はToArray()を使います。
4-4. ToListを使うべき場面と使いすぎに注意すべき場面
ToList()は便利ですが、使いすぎには注意が必要です。
ToList()を使うべき場面は、たとえば次のような場合です。
| 場面 | 理由 |
|---|---|
| 結果を何度も使い回す | 毎回LINQが再実行されるのを防げる |
| 要素数を何度も確認する | ListのCountプロパティを使える |
| インデックスアクセスしたい | list[0]のように取得できる |
| 追加・削除したい | Listのメソッドを使える |
| DB取得結果を確定したい | クエリ実行タイミングを明確にできる |
一方、単にforeachで1回だけ処理する場合は、無理にToList()する必要はありません。
C#IEnumerable<int> evenNumbers = numbers.Where(n => n % 2 == 0);
foreach (int number in evenNumbers)
{
Console.WriteLine(number);
}
このような場合、ToList()を使わないほうが余計なメモリを使わずに済むことがあります。
C#foreach (int number in numbers.Where(n => n % 2 == 0))
{
Console.WriteLine(number);
}
ToList()は「結果を確定させたいとき」に使う、と覚えるとよいです。
4-5. 変換時に発生しやすいエラーと対処法
IEnumerableとListの変換でよくあるエラーの1つが、ToList()が見つからないというものです。
C#IEnumerable<int> numbers = new int[] { 1, 2, 3 };
// List<int> list = numbers.ToList(); // ToListが見つからない場合がある
この場合は、System.Linqをインポートします。
C#using System.Linq;
もう1つよくあるのが、IEnumerable<T>をそのままList<T>に代入しようとするエラーです。
C#IEnumerable<int> numbers = new int[] { 1, 2, 3 };
// List<int> list = numbers; // エラー
この場合は、ToList()を使います。
C#List<int> list = numbers.ToList();
また、LINQの結果を複数回使う場合、毎回再評価される可能性があります。
C#IEnumerable<int> result = numbers.Where(n => n > 10);
int count = result.Count();
foreach (int number in result)
{
Console.WriteLine(number);
}
この場合、必要に応じてToList()で結果を確定させます。
C#List<int> result = numbers
.Where(n => n > 10)
.ToList();
5. IEnumerableとLINQの関係
5-1. LINQはIEnumerableに対して使える便利な機能
LINQは、コレクションに対して検索、絞り込み、変換、並び替えなどを行うための機能です。
多くのLINQメソッドは、IEnumerable<T>に対して使えます。
C#List<int> numbers = new List<int>
{
1,
2,
3,
4,
5
};
IEnumerable<int> result = numbers.Where(n => n >= 3);
このように、Listに対してWhereを呼べるのは、ListがIEnumerable<T>を実装しているからです。
つまり、LINQを使う場面では、IEnumerable<T>の理解がとても重要です。
LINQを使うと、従来のforeachやifを使った処理を短く読みやすく書ける場合があります。
5-2. Whereで条件に合う要素を絞り込む
Whereは、条件に合う要素だけを取り出すLINQメソッドです。
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
文字列の絞り込みもできます。
C#List<string> names = new List<string>
{
"Tanaka",
"Sato",
"Suzuki"
};
IEnumerable<string> result = names.Where(name => name.Contains("a"));
Whereの結果は、条件に合う要素を順番に取り出せるIEnumerable<T>です。
5-3. Selectで要素を変換する
Selectは、要素を別の形に変換するLINQメソッドです。
C#List<string> names = new List<string>
{
"田中",
"佐藤",
"鈴木"
};
IEnumerable<string> messages = names.Select(name => $"{name}さん");
結果を出力すると、次のようになります。
C#foreach (string message in messages)
{
Console.WriteLine(message);
}
C#田中さん
佐藤さん
鈴木さん
オブジェクトから特定のプロパティだけを取り出す場合にも使います。
C#List<User> users = new List<User>
{
new User { Id = 1, Name = "田中" },
new User { Id = 2, Name = "佐藤" }
};
IEnumerable<string> userNames = users.Select(user => user.Name);
Selectを使うと、データの形を表示用やDTO用に変換しやすくなります。
5-4. OrderByで並び替える
OrderByは、要素を昇順に並び替えるLINQメソッドです。
C#List<int> numbers = new List<int>
{
3,
1,
5,
2,
4
};
IEnumerable<int> sortedNumbers = numbers.OrderBy(n => n);
出力すると、次のようになります。
C#foreach (int number in sortedNumbers)
{
Console.WriteLine(number);
}
C#1
2
3
4
5
降順に並び替える場合は、OrderByDescendingを使います。
C#IEnumerable<int> sortedDesc = numbers.OrderByDescending(n => n);
オブジェクトのプロパティで並び替えることもできます。
C#IEnumerable<User> sortedUsers = users.OrderBy(user => user.Age);
複数条件で並び替える場合は、ThenByを使います。
C#IEnumerable<User> sortedUsers = users
.OrderBy(user => user.LastName)
.ThenBy(user => user.FirstName);
5-5. FirstOrDefault・Any・Countの使い方
FirstOrDefaultは、最初の要素を取得します。要素が存在しない場合は、型の既定値を返します。
C#List<int> numbers = new List<int>
{
1,
2,
3
};
int first = numbers.FirstOrDefault();
条件を指定することもできます。
C#int firstEven = numbers.FirstOrDefault(n => n % 2 == 0);
Anyは、要素が存在するかどうかを判定します。
C#bool hasAny = numbers.Any();
bool hasEven = numbers.Any(n => n % 2 == 0);
空かどうかを判定する場合は、Count() > 0よりもAny()のほうが適していることが多いです。
C#if (numbers.Any())
{
Console.WriteLine("要素があります");
}
Countは、要素数を取得します。
C#int count = numbers.Count();
int evenCount = numbers.Count(n => n % 2 == 0);
ただし、IEnumerable<T>に対するCount()は、対象によっては全要素を数えるため、何度も呼ぶとパフォーマンスに影響する場合があります。
5-6. LINQの結果がIEnumerableになる理由
LINQのWhereやSelectの結果は、多くの場合IEnumerable<T>になります。
C#IEnumerable<int> result = numbers.Where(n => n > 3);
これは、LINQの結果が「条件に合う要素を順番に取り出せるもの」として表現されるからです。
また、LINQには遅延実行という仕組みがあります。
C#IEnumerable<int> result = numbers.Where(n => n > 3);
この時点では、まだ実際の絞り込み処理が行われていない場合があります。
実際に処理されるのは、foreachやToList()などで列挙されたタイミングです。
C#foreach (int number in result)
{
Console.WriteLine(number);
}
この仕組みにより、必要なタイミングで必要な分だけ処理できる場合があります。
5-7. foreachとLINQの書き方を比較
偶数だけを取り出す処理を、まずforeachで書いてみます。
C#List<int> numbers = new List<int>
{
1,
2,
3,
4,
5
};
List<int> evenNumbers = new List<int>();
foreach (int number in numbers)
{
if (number % 2 == 0)
{
evenNumbers.Add(number);
}
}
同じ処理をLINQで書くと、次のようになります。
C#IEnumerable<int> evenNumbers = numbers.Where(n => n % 2 == 0);
LINQを使うと、何をしたいのかが簡潔に表現できます。
さらに、変換も組み合わせられます。
C#IEnumerable<string> messages = numbers
.Where(n => n % 2 == 0)
.Select(n => $"{n}は偶数です");
foreachは細かい処理を1つずつ書きたい場合に向いています。
LINQは、絞り込み、変換、並び替えなどを宣言的に書きたい場合に向いています。
6. IEnumerableの重要ポイント「遅延実行」を理解する
6-1. 遅延実行とは何か
IEnumerable<T>を理解するうえで重要なのが、遅延実行です。
遅延実行とは、LINQの処理が定義された時点ではすぐに実行されず、実際に要素が必要になったタイミングで実行される仕組みです。
次のコードを見てください。
C#List<int> numbers = new List<int>
{
1,
2,
3,
4,
5
};
IEnumerable<int> result = numbers.Where(n =>
{
Console.WriteLine($"{n}を判定中");
return n % 2 == 0;
});
Console.WriteLine("foreach開始");
foreach (int number in result)
{
Console.WriteLine(number);
}
Whereを書いた時点では、まだ判定処理は実行されません。
実際にforeachで列挙したタイミングで、要素が1つずつ処理されます。
6-2. LINQが実行されるタイミング
LINQは、主に次のようなタイミングで実行されます。
| 操作 | 実行されるか |
|---|---|
Whereを変数に代入 | まだ実行されないことが多い |
Selectを変数に代入 | まだ実行されないことが多い |
foreachで回す | 実行される |
ToList()を呼ぶ | 実行される |
ToArray()を呼ぶ | 実行される |
Count()を呼ぶ | 実行される |
FirstOrDefault()を呼ぶ | 実行される |
Any()を呼ぶ | 実行される |
たとえば、次の時点ではまだ実行されません。
C#IEnumerable<int> result = numbers.Where(n => n > 3);
次のように列挙したときに実行されます。
C#foreach (int number in result)
{
Console.WriteLine(number);
}
また、ToList()を呼んだ時点でも実行されます。
C#List<int> list = result.ToList();
6-3. ToListを呼ぶと即時実行される理由
ToList()は、IEnumerable<T>の中身をすべて列挙して、新しいListを作ります。
そのため、ToList()を呼んだ時点でLINQの処理が実行されます。
C#IEnumerable<int> query = numbers.Where(n =>
{
Console.WriteLine($"{n}を判定中");
return n % 2 == 0;
});
List<int> list = query.ToList();
この場合、ToList()の行でWhereの処理が実行されます。
ToList()を使うと、遅延実行されていた結果をその時点で確定できます。
これは、次のような場面で役立ちます。
C#List<User> activeUsers = users
.Where(user => user.IsActive)
.ToList();
このコードでは、アクティブユーザーの一覧をListとして確定しています。
後続処理で何度も使うなら、ToList()で確定しておくとわかりやすい場合があります。
6-4. 遅延実行で起こりやすい初心者のつまずき
遅延実行では、元のコレクションを後から変更すると、結果も変わることがあります。
C#List<int> numbers = new List<int>
{
1,
2,
3
};
IEnumerable<int> result = numbers.Where(n => n >= 2);
numbers.Add(4);
foreach (int number in result)
{
Console.WriteLine(number);
}
出力は次のようになります。
C#2
3
4
resultを作った時点では4は存在していませんでした。
しかし、foreachで実行されるタイミングではListに4が追加されているため、結果に含まれます。
作成時点の結果を固定したい場合は、ToList()を使います。
C#List<int> result = numbers
.Where(n => n >= 2)
.ToList();
numbers.Add(4);
foreach (int number in result)
{
Console.WriteLine(number);
}
この場合、結果は2と3だけになります。
6-5. 遅延実行のメリットとデメリット
遅延実行にはメリットがあります。
必要になるまで処理を実行しないため、無駄な処理を避けられる場合があります。
C#IEnumerable<int> result = numbers
.Where(n => n > 0)
.Select(n => n * 10);
この時点では処理が実行されないため、実際に使われない場合は処理コストをかけずに済みます。
また、大量データを扱う場合、すべてを一度にList化せず、必要な分だけ処理できることがあります。
一方で、デメリットもあります。
| デメリット | 内容 |
|---|---|
| 実行タイミングがわかりにくい | 代入時ではなく列挙時に実行される |
| 複数回列挙すると再実行される | foreachするたびに処理される場合がある |
| 元データ変更の影響を受ける | 後から追加・削除した内容が反映される場合がある |
| デバッグしづらい | 中身が確定していないことがある |
初心者のうちは、結果を固定したい場面ではToList()を使うと理解しやすくなります。
6-6. デバッグ時にIEnumerableの中身がわかりにくい理由
デバッグ中にIEnumerable<T>の変数を見ると、中身が直感的にわかりにくいことがあります。
これは、IEnumerable<T>が必ずしもすでにデータを持っているとは限らないからです。
たとえば、次のresultは処理の手順を表しているだけで、まだ結果が確定していない場合があります。
C#IEnumerable<int> result = numbers.Where(n => n > 3);
デバッガで中身を見ると、そのタイミングで列挙が発生することもあります。
中身を明確に確認したい場合は、一時的にToList()を使うと便利です。
C#List<int> debugList = result.ToList();
ただし、本番コードでむやみにToList()を追加すると、メモリ使用量や処理タイミングに影響することがあります。
デバッグ目的なのか、設計上必要なのかを分けて考えることが大切です。
7. IEnumerable・ICollection・IList・IQueryableの違い
7-1. ICollectionとの違い
ICollection<T>は、IEnumerable<T>よりも多くの機能を持つインターフェースです。
IEnumerable<T>は、基本的に「順番に取り出せる」ことを表します。
一方、ICollection<T>は、要素数の取得や追加・削除などの操作も表します。
C#ICollection<string> names = new List<string>
{
"田中",
"佐藤"
};
names.Add("鈴木");
Console.WriteLine(names.Count);
ICollection<T>には、主に次のような機能があります。
| 機能 | 説明 |
|---|---|
Count | 要素数を取得する |
Add | 要素を追加する |
Remove | 要素を削除する |
Clear | すべて削除する |
Contains | 含まれているか確認する |
読み取りだけならIEnumerable<T>、要素数や追加・削除も必要ならICollection<T>を検討します。
7-2. IListとの違い
IList<T>は、ICollection<T>よりさらに多くの機能を持つインターフェースです。
大きな特徴は、インデックスアクセスができることです。
C#IList<string> names = new List<string>
{
"田中",
"佐藤",
"鈴木"
};
Console.WriteLine(names[0]);
names[1] = "高橋";
IList<T>は、Listのように順番と位置を意識して操作したい場合に向いています。
IEnumerable<T>、ICollection<T>、IList<T>の関係を簡単に整理すると、次のようになります。
| 型 | 主な特徴 |
|---|---|
IEnumerable<T> | 順番に取り出せる |
ICollection<T> | 要素数取得、追加、削除ができる |
IList<T> | インデックスアクセスができる |
必要な機能が少ないほど、より抽象的な型を使えます。
7-3. IQueryableとの違い
IQueryable<T>は、主にデータベースなど外部データソースへの問い合わせで使われるインターフェースです。
IEnumerable<T>は、基本的にはメモリ上のコレクションを順番に処理するイメージです。
一方、IQueryable<T>は、LINQの式をSQLなどに変換して、データベース側で実行できる場合があります。
C#IQueryable<User> query = dbContext.Users
.Where(user => user.IsActive);
この時点では、まだデータベースに問い合わせていないことが多いです。
その後、ToList()などを呼ぶとSQLが実行され、結果が取得されます。
C#List<User> users = query.ToList();
IEnumerable<T>とIQueryable<T>の違いは、Entity Frameworkなどを使うときに特に重要です。
7-4. Entity FrameworkでIEnumerableとIQueryableを使い分ける考え方
Entity Frameworkでは、IQueryable<T>のまま条件を組み立てると、SQLに変換されてデータベース側で処理されます。
C#IQueryable<User> query = dbContext.Users;
query = query.Where(user => user.IsActive);
query = query.Where(user => user.Age >= 20);
List<User> users = query.ToList();
この場合、条件を含んだSQLが実行されるため、必要なデータだけをデータベースから取得しやすくなります。
一方、途中でAsEnumerable()やToList()を呼ぶと、その後の処理はメモリ上で行われる場合があります。
C#IEnumerable<User> users = dbContext.Users
.AsEnumerable()
.Where(user => IsTargetUser(user));
独自のC#メソッドを使いたい場合などはIEnumerable<T>にすることがありますが、大量データを扱う場合は注意が必要です。
データベースから全件取得してからメモリ上で絞り込むと、パフォーマンスが悪くなる可能性があります。
基本的には、データベースで絞り込める条件はIQueryable<T>の段階で書き、必要なタイミングでToList()するのがよくある考え方です。
7-5. 戻り値や引数ではどの型を選ぶべきか
戻り値や引数でどの型を使うべきかは、必要な操作によって決めます。
| 目的 | 選ぶ型の例 |
|---|---|
| 順番に読むだけ | IEnumerable<T> |
| 要素数を頻繁に使う | IReadOnlyCollection<T>やICollection<T> |
| インデックスアクセスしたい | IReadOnlyList<T>やIList<T> |
| 追加・削除したい | ICollection<T>やList<T> |
| DBクエリを組み立てたい | IQueryable<T> |
実務では、引数にはできるだけ柔軟な型を使い、戻り値には呼び出し側に必要な機能だけを公開することが多いです。
たとえば、要素を読むだけのメソッドなら次のようにします。
C#void SendEmails(IEnumerable<User> users)
{
foreach (User user in users)
{
Console.WriteLine($"{user.Email}に送信");
}
}
呼び出し側にインデックスアクセスや要素数を保証したい場合は、IReadOnlyList<T>などを検討します。
C#IReadOnlyList<User> GetUsers()
{
return users.ToList();
}
「何ができる必要があるか」を基準に型を選ぶことが大切です。
8. IEnumerableを使うメリット
8-1. List以外のコレクションも同じように扱える
IEnumerable<T>を使う大きなメリットは、List以外のコレクションも同じように扱えることです。
C#void PrintItems(IEnumerable<string> items)
{
foreach (string item in items)
{
Console.WriteLine(item);
}
}
このメソッドには、Listも配列も渡せます。
C#PrintItems(new List<string> { "A", "B" });
PrintItems(new string[] { "C", "D" });
HashSetも渡せます。
C#HashSet<string> set = new HashSet<string>
{
"E",
"F"
};
PrintItems(set);
もし引数がList<string>だった場合、配列やHashSetをそのまま渡せません。
IEnumerable<T>を使うことで、メソッドの利用範囲が広がります。
8-2. メソッドの柔軟性が高くなる
引数にIEnumerable<T>を使うと、呼び出し側の自由度が高くなります。
C#int Sum(IEnumerable<int> numbers)
{
int total = 0;
foreach (int number in numbers)
{
total += number;
}
return total;
}
このメソッドは、Listでも配列でもLINQの結果でも使えます。
C#int total1 = Sum(new List<int> { 1, 2, 3 });
int total2 = Sum(new int[] { 4, 5, 6 });
IEnumerable<int> evenNumbers = Enumerable.Range(1, 10)
.Where(n => n % 2 == 0);
int total3 = Sum(evenNumbers);
メソッド側は「順番に取り出せればよい」だけなので、具体的なコレクション型を指定する必要がありません。
このように、必要最小限の型を使うと、コードの再利用性が高くなります。
8-3. LINQと組み合わせてコードを簡潔に書ける
IEnumerable<T>はLINQと相性が非常によいです。
C#IEnumerable<User> activeUsers = users
.Where(user => user.IsActive)
.OrderBy(user => user.Name);
さらに、表示用データに変換できます。
C#IEnumerable<string> displayNames = users
.Where(user => user.IsActive)
.OrderBy(user => user.Name)
.Select(user => $"{user.Name} ({user.Email})");
このように、絞り込み、並び替え、変換をチェーンで書けます。
foreachで同じ処理を書くと長くなる場合でも、LINQを使うと意図が読み取りやすくなります。
ただし、処理が複雑になりすぎると逆に読みにくくなるため、適度に変数へ分けることも大切です。
8-4. 必要な分だけ処理できるため効率的な場合がある
IEnumerable<T>とLINQの遅延実行を活用すると、必要な分だけ処理できる場合があります。
たとえば、次のコードでは、条件に合う最初の要素だけを探します。
C#int first = numbers
.Where(n => n > 100)
.FirstOrDefault();
FirstOrDefaultは最初の要素が見つかれば処理を終えられるため、すべての要素を最後まで処理しなくてよい場合があります。
また、yield returnを使うと、要素を1つずつ生成するIEnumerable<T>を作れます。
C#IEnumerable<int> GetNumbers()
{
for (int i = 1; i <= 100; i++)
{
yield return i;
}
}
このような書き方では、必要になったタイミングで値を返せます。
大量データやストリーム的な処理では、すべてを一度にList化しないほうが効率的な場合があります。
8-5. 外部からコレクションを変更されにくい設計にできる
戻り値をIEnumerable<T>にすると、呼び出し側から直接AddやRemoveを呼びにくくなります。
C#private List<User> users = new List<User>();
public IEnumerable<User> GetUsers()
{
return users;
}
呼び出し側では、次のような操作はできません。
C#IEnumerable<User> result = GetUsers();
// result.Add(new User()); // エラー
これにより、外部から意図せずコレクションを変更されることを防ぎやすくなります。
ただし、完全に変更を防げるわけではありません。実体がListであることを知っていればキャストされる可能性もあります。
より厳密に読み取り専用にしたい場合は、IReadOnlyList<T>やコピーを返す設計も検討します。
C#public IReadOnlyList<User> GetUsers()
{
return users.AsReadOnly();
}
それでも、IEnumerable<T>は「読み取り中心のAPIである」という意図を伝えるのに役立ちます。
9. IEnumerableを使うときの注意点
9-1. 複数回foreachすると再実行される場合がある
IEnumerable<T>は、複数回列挙すると処理が再実行される場合があります。
C#IEnumerable<int> result = numbers.Where(n =>
{
Console.WriteLine($"{n}を判定");
return n % 2 == 0;
});
foreach (int number in result)
{
Console.WriteLine(number);
}
foreach (int number in result)
{
Console.WriteLine(number);
}
この場合、Whereの判定処理が2回実行される可能性があります。
単純なListであれば問題になりにくいですが、DBアクセスやAPI呼び出しが関係する場合は注意が必要です。
結果を何度も使うなら、ToList()で確定しておくとよいです。
C#List<int> result = numbers
.Where(n => n % 2 == 0)
.ToList();
9-2. Countを何度も呼ぶとパフォーマンスが悪くなる場合がある
List<T>のCountプロパティは、要素数をすぐに取得できます。
C#int count = list.Count;
一方、IEnumerable<T>に対するLINQのCount()は、対象によっては全要素を列挙して数えることがあります。
C#int count = enumerable.Count();
これを何度も呼ぶと、毎回列挙が発生してパフォーマンスが悪くなる場合があります。
C#if (items.Count() > 0)
{
Console.WriteLine(items.Count());
}
空かどうかを確認したいだけなら、Any()を使うほうが適しています。
C#if (items.Any())
{
Console.WriteLine("要素があります");
}
要素数を何度も使うなら、ToList()してからCountプロパティを使う方法もあります。
C#List<int> list = items.ToList();
Console.WriteLine(list.Count);
9-3. DBアクセスやAPI取得と組み合わせるときの注意
IEnumerable<T>をDBアクセスやAPI取得と組み合わせる場合は、実行タイミングに注意が必要です。
たとえば、Entity Frameworkでは、ToList()を呼ぶまでSQLが実行されないことがあります。
C#IQueryable<User> query = dbContext.Users
.Where(user => user.IsActive);
List<User> users = query.ToList();
この場合、ToList()の時点でDBに問い合わせが行われます。
一方、IEnumerable<T>を返すメソッドで内部的にDB接続を使っている場合、列挙タイミングが遅れることで問題が起きることがあります。
C#IEnumerable<User> GetUsers()
{
using var context = new AppDbContext();
return context.Users.Where(user => user.IsActive);
}
このようなコードでは、メソッドを抜けた時点でcontextが破棄され、その後に列挙しようとしてエラーになる可能性があります。
この場合は、メソッド内でToList()して結果を確定させます。
C#List<User> GetUsers()
{
using var context = new AppDbContext();
return context.Users
.Where(user => user.IsActive)
.ToList();
}
DBやAPIなど外部リソースを使う場合は、「いつ実行されるか」を意識することが重要です。
9-4. nullチェックと空コレクションの扱い
IEnumerable<T>を扱うときは、nullと空コレクションを区別しましょう。
C#IEnumerable<string>? names = null;
この状態でforeachすると、エラーになります。
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);
}
空かどうかを判定する場合は、Any()を使います。
C#if (!names.Any())
{
Console.WriteLine("空です");
}
ただし、names自体がnullの可能性がある場合は、先にnullチェックが必要です。
9-5. IEnumerableを返すときにToListすべきか判断するポイント
メソッドの戻り値でIEnumerable<T>を返すとき、内部でToList()すべきか迷うことがあります。
判断のポイントは、結果をいつ確定させたいかです。
遅延実行のままでよい場合は、そのまま返しても構いません。
C#IEnumerable<int> GetEvenNumbers(IEnumerable<int> numbers)
{
return numbers.Where(n => n % 2 == 0);
}
この場合、呼び出し側が列挙したタイミングで処理されます。
一方、メソッド内で結果を確定させたい場合は、ToList()します。
C#IEnumerable<int> GetEvenNumbers(IEnumerable<int> numbers)
{
return numbers
.Where(n => n % 2 == 0)
.ToList();
}
特に、次のような場合はToList()を検討します。
| 場面 | ToListを検討する理由 |
|---|---|
| DB接続をメソッド内で閉じる | 後から列挙するとエラーになる可能性がある |
| API結果を固定したい | 再実行を防ぎたい |
| 結果を複数回使う | 再列挙を避けたい |
| 元データ変更の影響を避けたい | 作成時点の結果を保持したい |
ただし、常にToList()すればよいわけではありません。大量データの場合、すべてをメモリに載せることになるため注意が必要です。
10. IEnumerableの実践サンプル
10-1. 数値リストを条件で絞り込む
数値リストから偶数だけを取り出す例です。
C#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
10以上の数値だけを取り出す場合は、次のように書けます。
C#IEnumerable<int> result = numbers.Where(n => n >= 10);
条件を変えるだけで、さまざまな絞り込みができます。
10-2. 文字列リストから特定の文字を含む要素を抽出する
文字列の一覧から、特定の文字を含む要素だけを取り出す例です。
C#List<string> names = new List<string>
{
"Tanaka",
"Sato",
"Suzuki",
"Takahashi"
};
IEnumerable<string> result = names.Where(name => name.Contains("ta", StringComparison.OrdinalIgnoreCase));
foreach (string name in result)
{
Console.WriteLine(name);
}
大文字・小文字を区別せずに検索したい場合は、StringComparison.OrdinalIgnoreCaseを使うと便利です。
日本語の文字列でも使えます。
C#List<string> products = new List<string>
{
"ノートパソコン",
"デスクトップパソコン",
"スマートフォン",
"タブレット"
};
IEnumerable<string> result = products.Where(product => product.Contains("パソコン"));
部分一致検索は、画面の検索機能やフィルター機能でよく使われます。
10-3. オブジェクトの一覧から条件検索する
実務では、数値や文字列だけでなく、オブジェクトの一覧を扱うことが多いです。
C#public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
public int Age { get; set; }
public bool IsActive { get; set; }
}
ユーザー一覧から、有効なユーザーだけを取り出してみます。
C#List<User> users = new List<User>
{
new User { Id = 1, Name = "田中", Age = 25, IsActive = true },
new User { Id = 2, Name = "佐藤", Age = 17, IsActive = true },
new User { Id = 3, Name = "鈴木", Age = 30, IsActive = false }
};
IEnumerable<User> activeUsers = users.Where(user => user.IsActive);
さらに、20歳以上の有効ユーザーだけに絞り込めます。
C#IEnumerable<User> result = users
.Where(user => user.IsActive)
.Where(user => user.Age >= 20);
条件は1つのWhereにまとめても構いません。
C#IEnumerable<User> result = users
.Where(user => user.IsActive && user.Age >= 20);
10-4. SelectでDTOや表示用データに変換する
Selectを使うと、オブジェクトを別の形に変換できます。
たとえば、Userから表示用の文字列を作る例です。
C#IEnumerable<string> displayNames = users
.Select(user => $"{user.Name}さん({user.Age}歳)");
DTOに変換する例も見てみましょう。
C#public class UserDto
{
public int Id { get; set; }
public string DisplayName { get; set; } = "";
}
C#IEnumerable<UserDto> userDtos = users.Select(user => new UserDto
{
Id = user.Id,
DisplayName = $"{user.Name}さん"
});
このように、Selectはデータベースのエンティティを画面表示用のデータに変換するときによく使います。
絞り込みと組み合わせることもできます。
C#IEnumerable<UserDto> activeUserDtos = users
.Where(user => user.IsActive)
.Select(user => new UserDto
{
Id = user.Id,
DisplayName = $"{user.Name}さん"
});
10-5. yield returnでIEnumerableを自作する
yield returnを使うと、IEnumerable<T>を返すメソッドを簡単に作れます。
C#IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
yield return 3;
}
呼び出し側では、通常のIEnumerable<int>として扱えます。
C#foreach (int number in GetNumbers())
{
Console.WriteLine(number);
}
ループと組み合わせることもできます。
C#IEnumerable<int> GetEvenNumbers(int max)
{
for (int i = 1; i <= max; i++)
{
if (i % 2 == 0)
{
yield return i;
}
}
}
使い方は次のとおりです。
C#foreach (int number in GetEvenNumbers(10))
{
Console.WriteLine(number);
}
yield returnを使うと、すべての結果をListに詰めてから返す必要がありません。
C#IEnumerable<int> GetEvenNumbers(int max)
{
List<int> results = new List<int>();
for (int i = 1; i <= max; i++)
{
if (i % 2 == 0)
{
results.Add(i);
}
}
return results;
}
このようなコードを、より簡潔に書けます。
10-6. 実務でよくあるメソッド設計例
実務では、メソッドの引数にIEnumerable<T>を使う場面が多くあります。
たとえば、ユーザー一覧に対してメールを送る処理です。
C#void SendNotification(IEnumerable<User> users)
{
foreach (User user in users)
{
Console.WriteLine($"{user.Name}に通知を送信しました");
}
}
このメソッドは、ListでもLINQの結果でも受け取れます。
C#List<User> users = GetAllUsers();
IEnumerable<User> activeUsers = users.Where(user => user.IsActive);
SendNotification(activeUsers);
検索処理の戻り値として使うこともあります。
C#IEnumerable<User> FindActiveUsers(IEnumerable<User> users)
{
return users.Where(user => user.IsActive);
}
ただし、DBから取得する場合は、戻り値をList<T>にする設計もよくあります。
C#List<User> GetActiveUsers()
{
using var context = new AppDbContext();
return context.Users
.Where(user => user.IsActive)
.ToList();
}
このように、メモリ上のコレクションを処理するのか、DBアクセスを含むのかによって、IEnumerable<T>のまま返すかList<T>で確定するかを判断します。
11. IEnumerableでよくあるエラー・疑問
11-1. IEnumerableにAddできないのはなぜ?
IEnumerable<T>にAddできない理由は、IEnumerable<T>が「順番に取り出す」ためのインターフェースだからです。
C#IEnumerable<string> names = new List<string>
{
"田中",
"佐藤"
};
// names.Add("鈴木"); // エラー
Addは、List<T>やICollection<T>などが持つ機能です。
追加したい場合は、Listとして扱う必要があります。
C#List<string> names = new List<string>
{
"田中",
"佐藤"
};
names.Add("鈴木");
すでにIEnumerable<T>として受け取ったデータに追加したい場合は、ToList()でListに変換します。
C#IEnumerable<string> names = GetNames();
List<string> list = names.ToList();
list.Add("鈴木");
ただし、ToList()すると新しいListが作られるため、元のコレクションとは別物になる点に注意しましょう。
11-2. IEnumerableの要素数を取得するには?
IEnumerable<T>の要素数を取得するには、LINQのCount()を使います。
C#IEnumerable<int> numbers = new List<int>
{
1,
2,
3
};
int count = numbers.Count();
条件に合う要素数を数えることもできます。
C#int evenCount = numbers.Count(n => n % 2 == 0);
ただし、Count()は対象によっては全要素を列挙する可能性があります。
要素数を何度も使う場合は、Listに変換してからCountプロパティを使うことも検討します。
C#List<int> list = numbers.ToList();
int count = list.Count;
空かどうかを知りたいだけなら、Count() > 0ではなくAny()を使うのがおすすめです。
C#if (numbers.Any())
{
Console.WriteLine("要素があります");
}
11-3. IEnumerableの先頭要素を取得するには?
IEnumerable<T>の先頭要素を取得するには、First()またはFirstOrDefault()を使います。
C#IEnumerable<string> names = new List<string>
{
"田中",
"佐藤",
"鈴木"
};
string first = names.First();
ただし、First()は要素が空の場合に例外が発生します。
C#IEnumerable<string> emptyNames = Enumerable.Empty<string>();
// string first = emptyNames.First(); // 例外
空の可能性がある場合は、FirstOrDefault()を使うと安全です。
C#string? first = emptyNames.FirstOrDefault();
条件に合う最初の要素を取得することもできます。
C#User? user = users.FirstOrDefault(user => user.IsActive);
「必ず存在する」と言い切れる場合はFirst()、存在しない可能性がある場合はFirstOrDefault()を使うとよいです。
11-4. IEnumerableをインデックスで取得したい場合は?
IEnumerable<T>は、基本的にインデックスアクセスできません。
C#IEnumerable<string> names = new List<string>
{
"田中",
"佐藤",
"鈴木"
};
// string name = names[0]; // エラー
インデックスで取得したい場合は、ElementAt()を使えます。
C#string name = names.ElementAt(0);
範囲外の可能性がある場合は、ElementAtOrDefault()を使います。
C#string? name = names.ElementAtOrDefault(10);
ただし、インデックスアクセスを頻繁に行うなら、Listに変換したほうがよい場合があります。
C#List<string> list = names.ToList();
string first = list[0];
IEnumerable<T>は順番に取り出すことに向いている型です。位置を指定して何度もアクセスするなら、List<T>やIReadOnlyList<T>を使うほうが自然です。
11-5. IEnumerableが空かどうか判定するには?
IEnumerable<T>が空かどうかを判定するには、Any()を使います。
C#IEnumerable<int> numbers = new List<int>();
if (!numbers.Any())
{
Console.WriteLine("空です");
}
要素がある場合は次のように書きます。
C#if (numbers.Any())
{
Console.WriteLine("要素があります");
}
Count() == 0でも判定できますが、IEnumerable<T>では全要素を数える場合があります。
C#if (numbers.Count() == 0)
{
Console.WriteLine("空です");
}
空かどうかだけ知りたいなら、Any()のほうが適しています。
ただし、numbers自体がnullの可能性がある場合は、nullチェックも必要です。
C#if (numbers == null || !numbers.Any())
{
Console.WriteLine("nullまたは空です");
}
11-6. 「IEnumerable is not List」と言われる理由
IEnumerableとListを混同すると、「IEnumerableはListではない」という意味のエラーや問題に遭遇することがあります。
たとえば、次のコードはエラーになります。
C#IEnumerable<int> numbers = new List<int>
{
1,
2,
3
};
// List<int> list = numbers; // エラー
理由は、IEnumerable<int>が必ずしもList<int>とは限らないからです。
IEnumerable<int>の実体は、配列かもしれません。
C#IEnumerable<int> numbers = new int[]
{
1,
2,
3
};
LINQの結果かもしれません。
C#IEnumerable<int> numbers = Enumerable.Range(1, 10)
.Where(n => n % 2 == 0);
そのため、IEnumerable<T>をそのままList<T>として扱うことはできません。
Listにしたい場合は、ToList()を使います。
C#List<int> list = numbers.ToList();
逆に、ListをIEnumerable<T>として扱うことはできます。
C#List<int> list = new List<int>
{
1,
2,
3
};
IEnumerable<int> numbers = list;
ListはIEnumerable<T>の一種ですが、IEnumerable<T>が常にListであるとは限らない、という点を理解しておきましょう。
まとめ
C#のIEnumerableは、「順番に要素を取り出せるもの」を表す重要なインターフェースです。
配列、List、Dictionaryなど多くのコレクションがIEnumerableを実装しており、foreachやLINQと深く関係しています。
IEnumerable<T>は、要素を読み取るだけの場面に向いています。一方、List<T>は、要素の追加、削除、更新、インデックスアクセスなどを行いたい場面に向いています。
IEnumerable<T>とList<T>の違いを簡単にまとめると、次のようになります。
| 型 | 向いている用途 |
|---|---|
IEnumerable<T> | 順番に読み取る、LINQで処理する、柔軟な引数にする |
List<T> | 要素を保持する、追加・削除する、インデックスでアクセスする |
また、LINQの多くの結果はIEnumerable<T>になり、遅延実行されます。WhereやSelectを書いた時点では実行されず、foreach、ToList()、Count()、FirstOrDefault()などで列挙されたタイミングで実行されます。
結果を確定したい場合はToList()、配列にしたい場合はToArray()を使います。ただし、むやみにToList()するとメモリ使用量が増えたり、処理タイミングが変わったりするため注意が必要です。
実務では、次のように考えると選びやすくなります。
| 判断基準 | 選ぶ型 |
|---|---|
| 読み取りだけでよい | IEnumerable<T> |
| 追加・削除したい | List<T>やICollection<T> |
| インデックスアクセスしたい | List<T>やIList<T> |
| DBクエリを組み立てたい | IQueryable<T> |
| 結果を固定したい | ToList()してList<T> |
csharp ienumerableを理解すると、C#のコレクション操作、LINQ、メソッド設計が一気にわかりやすくなります。
最初は「foreachできる型」「LINQの結果によく使われる型」と考えれば十分です。慣れてきたら、Listとの違い、遅延実行、IQueryableとの違いまで意識すると、より実務的で安全なC#コードを書けるようになります。

