C#のIEnumerableでCount()は遅い?要素数取得の仕組みとAny()との使い分けを徹底解説
はじめに
C#でIEnumerableを扱っていると、要素数を知るためにCount()を使う場面はよくあります。しかし、何気なく書いたitems.Count()が思ったより遅くなったり、Count() > 0よりAny()を使うべきだと指摘されたりして、判断に迷うこともあるでしょう。
結論から言うと、IEnumerableに対するCount()が常に遅いわけではありません。List<T>や配列のように要素数をすぐ取得できるコレクションなら高速です。一方で、yield returnで生成されたシーケンス、LINQのWhereやSelectを重ねたクエリ、ファイル・API・データベースなど外部リソースを列挙する処理では、Count()が全件列挙を引き起こし、パフォーマンス問題につながることがあります。
この記事では、C#のIEnumerableにおけるCount()の仕組み、CountプロパティやLengthとの違い、Any()との使い分け、Entity FrameworkなどDBアクセス時の注意点まで、実例コードを交えて解説します。
1. C#のIEnumerableでCount()を使うと本当に遅いのか
1-1. 結論:Count()が遅いケースと遅くないケース
IEnumerable<T>に対するCount()は、対象の実体によって速さが変わります。
たとえば、次のようなList<T>に対してCount()を呼ぶ場合、内部的には要素数を保持しているコレクションとして扱えるため、比較的高速に取得できます。
C#var numbers = new List<int> { 1, 2, 3 };
int count = numbers.Count();
一方で、次のようにyield returnで生成されるIEnumerable<T>や、LINQで加工されたクエリに対してCount()を呼ぶと、実際に最後まで列挙しないと件数が分からない場合があります。
C#IEnumerable<int> numbers = GetNumbers();
int count = numbers.Count();
Enumerable.Count()は、対象がICollection<T>を実装していればそのCountを利用し、そうでなければ件数を決定するために列挙します。つまり、「IEnumerable Countは遅い」と単純に覚えるのではなく、「要素数を直接取得できる型か、列挙しないと数えられない型か」で判断することが重要です。Microsoft Learn
1-2. 「IEnumerable Count」で検索する人が知りたいこと
「C# IEnumerable Count」「IEnumerable Count 遅い」「Count() Any() 違い」といったキーワードで検索する人が知りたいのは、主に次のような点です。
| 疑問 | 知るべきポイント |
|---|---|
Count()は毎回全件ループするのか | 対象の型による |
List<T>ならCount()でもよいのか | 基本はCountプロパティを使う |
| 空かどうかを調べるなら何を使うべきか | Count() > 0よりAny()が適切 |
IEnumerable<T>しかない場合はどうするか | 列挙コストや複数回列挙に注意する |
| Entity Frameworkではどうなるのか | IEnumerableとIQueryableで意味が変わる |
特に重要なのは、IEnumerable<T>は「要素数を持つ型」ではなく「列挙できる型」であるという点です。この違いを理解すると、Count()が速いケースと遅いケースを自然に判断できるようになります。
1-3. CountプロパティとLINQのCount()メソッドの違い
C#には、似た名前のCountプロパティとCount()メソッドがあります。
C#var list = new List<int> { 1, 2, 3 };
int a = list.Count; // Countプロパティ
int b = list.Count(); // LINQのCount()メソッド
Countプロパティは、List<T>やHashSet<T>などのコレクションが持つプロパティです。コレクション自身が要素数を管理しているため、通常は高速に取得できます。
一方、Count()はLINQの拡張メソッドです。System.Linq名前空間にあるEnumerable.Count()が呼ばれ、対象のIEnumerable<T>を調べて件数を返します。
使い分けの基本は次のとおりです。
C#// List<T>ならこちら
int count1 = list.Count;
// 配列ならこちら
int count2 = array.Length;
// IEnumerable<T>しかないならこちら
int count3 = sequence.Count();
List<T>だと分かっているなら、Count()ではなくCountプロパティを使うほうが意図が明確です。
1-4. まず押さえるべき要素数取得の判断基準
要素数を取得したいときは、次の順で考えると判断しやすくなります。
| 対象 | 推奨 |
|---|---|
| 配列 | Length |
List<T> | Count |
HashSet<T> | Count |
Dictionary<TKey, TValue> | Count |
ICollection<T> | Count |
IReadOnlyCollection<T> | Count |
IEnumerable<T>のみ | Count()、ただし列挙コストに注意 |
| 存在確認のみ | Any() |
要素数そのものが必要ならCountやCount()を使います。しかし、「1件でも存在するか」を知りたいだけなら、Count() > 0ではなくAny()を使うのが基本です。Any()はシーケンスに要素が存在するか、または条件を満たす要素が存在するかを判定するLINQメソッドです。Microsoft Learn
2. IEnumerableとは何か:要素数を直接持たない仕組み
2-1. IEnumerableは「列挙できる」ことを表すインターフェース
IEnumerable<T>は、C#で「順番に要素を取り出せる」ことを表すインターフェースです。foreachで回せる型の多くは、IEnumerable<T>を実装しています。
C#IEnumerable<int> numbers = new List<int> { 1, 2, 3 };
foreach (var number in numbers)
{
Console.WriteLine(number);
}
ここで大切なのは、IEnumerable<T>自体は「要素数を保持する」とは約束していないことです。あくまで「列挙できる」ことを表すだけです。
そのため、次のような処理では、numbersの実体が何かによってCount()のコストが変わります。
C#void PrintCount(IEnumerable<int> numbers)
{
Console.WriteLine(numbers.Count());
}
呼び出し元がList<int>を渡すのか、yield returnで生成したシーケンスを渡すのか、ファイルを1行ずつ読む処理を渡すのかによって、同じCount()でも意味が変わります。
2-2. 配列・List・HashSet・LINQクエリで挙動が変わる理由
IEnumerable<T>の変数に入っていても、実際のオブジェクトはさまざまです。
C#IEnumerable<int> a = new int[] { 1, 2, 3 };
IEnumerable<int> b = new List<int> { 1, 2, 3 };
IEnumerable<int> c = new HashSet<int> { 1, 2, 3 };
IEnumerable<int> d = Enumerable.Range(1, 100).Where(x => x % 2 == 0);
配列、List<T>、HashSet<T>のようなコレクションは、内部的に要素数を管理しています。そのため件数取得は得意です。
一方、WhereなどのLINQクエリは、「条件に合う要素を列挙するときに返す」という処理の流れを表している場合があります。この場合、条件に合う件数を知るには、実際に元のシーケンスを最後まで調べる必要があります。
C#var evenNumbers = Enumerable.Range(1, 100)
.Where(x => x % 2 == 0);
int count = evenNumbers.Count();
このコードでは、偶数が何件あるかを知るために、1から100までを評価する必要があります。
2-3. 遅延実行と再列挙がパフォーマンスに与える影響
LINQの多くのメソッドは遅延実行です。つまり、WhereやSelectを書いた時点では処理が実行されず、foreachやCount()、ToList()などで列挙されたタイミングで実行されます。
C#var query = numbers.Where(x =>
{
Console.WriteLine($"checking {x}");
return x > 10;
});
// ここではまだ実行されない
int count = query.Count(); // ここで実行される
さらに、同じIEnumerable<T>を複数回列挙すると、そのたびに処理が再実行される場合があります。
C#var query = numbers.Where(x => IsTarget(x));
int count = query.Count();
foreach (var item in query)
{
Console.WriteLine(item);
}
このコードでは、Count()で一度列挙し、foreachでもう一度列挙する可能性があります。IsTargetが重い処理なら、パフォーマンスに大きな影響が出ます。
必要に応じて、次のように一度リスト化することも検討します。
C#var list = numbers.Where(x => IsTarget(x)).ToList();
int count = list.Count;
foreach (var item in list)
{
Console.WriteLine(item);
}
ただし、ToList()はメモリを使うため、件数やデータサイズが大きい場合は注意が必要です。
2-4. IEnumerable<T>とICollection<T>・IReadOnlyCollection<T>の違い
IEnumerable<T>、ICollection<T>、IReadOnlyCollection<T>は似ていますが、役割が違います。
| インターフェース | 主な意味 | Countを持つか |
|---|---|---|
IEnumerable<T> | 列挙できる | 持たない |
ICollection<T> | 追加・削除などもできるコレクション | 持つ |
IReadOnlyCollection<T> | 読み取り専用のコレクション | 持つ |
IEnumerable<T>は最も抽象度が高く、柔軟です。しかし、要素数が必要な処理では情報が不足することがあります。
たとえば、メソッド内で必ず件数を使うなら、引数をIEnumerable<T>ではなくIReadOnlyCollection<T>にする設計も有効です。
C#void PrintSummary(IReadOnlyCollection<string> names)
{
Console.WriteLine($"件数: {names.Count}");
foreach (var name in names)
{
Console.WriteLine(name);
}
}
これにより、呼び出された側はCount()で列挙する必要がなくなります。
3. Count()メソッドの内部動作
3-1. Count()は常に全件ループするわけではない
Count()は「IEnumerable<T>を最初から最後まで必ずループする」と思われがちですが、実際にはそうとは限りません。
Enumerable.Count()は、対象が件数を直接取得できる型かどうかを確認します。対象がICollection<T>を実装している場合、そのCountを利用します。そうでなければ、列挙によって件数を数えます。Microsoft Learn
つまり、次の2つは見た目は似ていますが、実行コストが異なる可能性があります。
C#var list = new List<int> { 1, 2, 3 };
int count1 = list.Count();
var query = list.Where(x => x > 1);
int count2 = query.Count();
list.Count()は高速に件数を取得できる可能性が高い一方、query.Count()は条件を評価しながら件数を数える必要があります。
3-2. ICollection<T>やICollectionならCountプロパティを利用する
List<T>、HashSet<T>、Dictionary<TKey, TValue>などは、ICollection<T>または関連するコレクションインターフェースを実装しています。
そのため、次のようなコードでは、Count()を呼んでも内部的にコレクションのCountを利用できる場合があります。
C#IEnumerable<string> names = new List<string>
{
"Alice",
"Bob",
"Charlie"
};
int count = names.Count();
ただし、変数の型がList<string>であることが分かっているなら、わざわざLINQのCount()を使う必要はありません。
C#var names = new List<string>
{
"Alice",
"Bob",
"Charlie"
};
int count = names.Count;
このほうが読み手にも「コレクションの保持している件数を見ている」と伝わりやすくなります。
3-3. カウントできないIEnumerableでは最後まで列挙する
IEnumerable<T>の中には、要素数を事前に持っていないものがあります。
C#static IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
yield return 3;
}
このようなyield returnのシーケンスに対してCount()を呼ぶと、各要素を順番に取り出して数える必要があります。
C#int count = GetNumbers().Count();
件数が3件なら問題になりません。しかし、100万件のデータ、ファイル読み込み、ネットワーク越しのデータ取得などでは、Count()だけで大きな処理になることがあります。
3-4. .NET 6以降のTryGetNonEnumeratedCount()との関係
.NET 6以降では、TryGetNonEnumeratedCount()というメソッドが使えます。このメソッドは、シーケンスを列挙せずに件数を取得できるかを試み、取得できる場合はtrueと件数を返します。公式ドキュメントでは、ICollection<T>、ICollection、LINQ内部で使われる型などを判定対象にすると説明されています。Microsoft Learn
C#IEnumerable<int> numbers = new List<int> { 1, 2, 3 };
if (numbers.TryGetNonEnumeratedCount(out int count))
{
Console.WriteLine($"列挙せずに取得: {count}");
}
else
{
Console.WriteLine("列挙しないと件数は分からない");
}
TryGetNonEnumeratedCount()は、件数が取れるなら使い、取れないなら別の方法を考える、という分岐に向いています。
C#static void PrintInfo<T>(IEnumerable<T> source)
{
if (source.TryGetNonEnumeratedCount(out var count))
{
Console.WriteLine($"件数: {count}");
}
else
{
Console.WriteLine("件数は即時取得できません");
}
}
Count()は最終的に件数を返すため、必要であれば列挙します。一方、TryGetNonEnumeratedCount()は「列挙せずに取得できるか」を確認するためのメソッドです。
3-5. Count()で例外や副作用が起きる可能性があるケース
Count()は単に数字を返すだけに見えますが、対象によっては列挙中に例外や副作用が発生することがあります。
たとえば、次のようなシーケンスでは、Count()を呼ぶだけでConsole.WriteLineが実行されます。
C#static IEnumerable<int> GetNumbers()
{
Console.WriteLine("start");
yield return 1;
yield return 2;
Console.WriteLine("end");
}
int count = GetNumbers().Count();
また、ファイルを読むIEnumerable<string>なら、Count()によってファイル全体が読み込まれます。APIからページング取得するような実装なら、件数確認のつもりが大量の通信を発生させる可能性もあります。
さらに、sourceがnullの場合、Count()は正常には実行できません。
C#IEnumerable<int>? numbers = null;
// 例外になる
int count = numbers.Count();
nullの可能性がある場合は、事前にチェックするか、空シーケンスに置き換えます。
C#int count = numbers?.Count() ?? 0;
4. Count()が遅くなる典型パターン
4-1. yield returnで生成されたシーケンス
yield returnは、必要になったタイミングで要素を生成する仕組みです。便利ですが、要素数を事前に持っているわけではありません。
C#static IEnumerable<int> CreateNumbers(int max)
{
for (int i = 1; i <= max; i++)
{
yield return i;
}
}
int count = CreateNumbers(1_000_000).Count();
この場合、Count()は100万個の値を順番に列挙して数えます。
単にmaxが件数だと分かっているなら、Count()を使う必要はありません。
C#int max = 1_000_000;
int count = max;
もちろん、実際の処理では単純に置き換えられないこともあります。しかし、yield returnで作ったIEnumerable<T>にCount()を使うと、列挙が発生することは意識しておくべきです。
4-2. WhereやSelectなどLINQを重ねたクエリ
Whereで条件を絞り込んだ後のCount()は、条件に合う要素数を調べるために列挙が必要です。
C#var adults = users.Where(user => user.Age >= 20);
int count = adults.Count();
このコードは、usersを順番に調べて、Age >= 20を満たす要素を数えます。
Selectだけなら件数が変わらないように見えますが、LINQクエリの形や対象の型によっては、列挙が必要になることがあります。
C#var names = users.Select(user => user.Name);
int count = names.Count();
また、Where、Select、Distinct、OrderByなどを重ねると、Count()のために必要な処理が増える場合があります。
C#int count = users
.Where(user => user.IsActive)
.Select(user => user.Email)
.Distinct()
.Count();
このようなコードでは、「本当に件数が必要なのか」「存在確認だけでよいのか」「事前にリスト化すべきか」を考えることが大切です。
4-3. データベース・ファイル・APIなど外部リソースを列挙する場合
IEnumerable<T>の背後に外部リソースがある場合、Count()のコストはさらに大きくなります。
代表例は次のとおりです。
| 対象 | Count()で起こり得ること |
|---|---|
File.ReadLines() | ファイルを最後まで読む |
| APIのページング結果 | 全ページ取得が発生する |
| ストリーム処理 | 終端まで読み進める |
DB結果をAsEnumerable()したもの | クライアント側で列挙する可能性がある |
たとえば、File.ReadLines()は遅延的にファイルを読みます。
C#var lines = File.ReadLines("large.log");
int count = lines.Count();
このコードは、行数を数えるためにファイル全体を読みます。巨大なログファイルでは、Count()だけでも重い処理になります。
4-4. 無限シーケンスにCount()を使った場合
IEnumerable<T>は、理論上は無限に続くシーケンスも表現できます。
C#static IEnumerable<int> Infinite()
{
int i = 0;
while (true)
{
yield return i++;
}
}
このシーケンスにCount()を使うと、最後がないため処理が終わりません。
C#int count = Infinite().Count(); // 終わらない
無限シーケンスや終端が保証されないシーケンスでは、Count()を使ってはいけません。
必要な範囲だけを対象にするなら、Take()などで制限します。
C#int count = Infinite()
.Take(100)
.Count();
4-5. Count()をループ内で何度も呼び出すアンチパターン
特に注意したいのが、ループ内でCount()を何度も呼ぶコードです。
C#IEnumerable<int> numbers = GetNumbers();
for (int i = 0; i < numbers.Count(); i++)
{
Console.WriteLine(i);
}
numbersがList<int>なら大きな問題になりにくいかもしれません。しかし、numbersが毎回列挙を必要とするIEnumerable<int>だった場合、ループのたびに全件カウントが発生します。
改善するなら、先に件数を変数に入れます。
C#int count = numbers.Count();
for (int i = 0; i < count; i++)
{
Console.WriteLine(i);
}
さらに、インデックスアクセスが必要なら、IEnumerable<T>ではなくList<T>にする設計も検討します。
C#var list = numbers.ToList();
for (int i = 0; i < list.Count; i++)
{
Console.WriteLine(list[i]);
}
ただし、ToList()も全件列挙とメモリ確保を伴うため、データ量が大きい場合は慎重に使いましょう。
5. Any()との使い分け:存在確認ならCount() > 0ではなくAny()
5-1. Any()は1件見つかれば判定できる
Any()は、シーケンスに要素が1件でも存在するかを判定するメソッドです。
C#if (items.Any())
{
Console.WriteLine("要素があります");
}
Any()の大きな利点は、1件見つかればそこで判定できることです。すべての要素数を数える必要はありません。
条件付きのAny()も同様です。
C#if (users.Any(user => user.IsActive))
{
Console.WriteLine("有効なユーザーが存在します");
}
この場合、有効なユーザーが1件見つかれば、それ以上の列挙は不要です。
5-2. Count() > 0が非効率になりやすい理由
次のようなコードはよく見かけます。
C#if (items.Count() > 0)
{
Console.WriteLine("要素があります");
}
このコードは間違いではありません。しかし、itemsが要素数を直接取得できないIEnumerable<T>だった場合、Count()は全件を数える可能性があります。
存在確認だけなら、全件数える必要はありません。
C#if (items.Any())
{
Console.WriteLine("要素があります");
}
Count() > 0は「件数を数えてから0より大きいかを見る」書き方です。一方、Any()は「存在するかどうかを見る」書き方です。意図としてもAny()のほうが明確です。
5-3. 条件付き判定はCount(predicate)よりAny(predicate)が適切なケース
条件に合う要素が存在するかだけを知りたい場合、Count(predicate) > 0ではなくAny(predicate)を使います。
C#// 非推奨になりやすい
if (users.Count(user => user.IsActive) > 0)
{
Console.WriteLine("有効なユーザーがいます");
}
このコードは、有効なユーザーが何人いるかを最後まで数えます。
存在確認だけなら、次のように書きます。
C#if (users.Any(user => user.IsActive))
{
Console.WriteLine("有効なユーザーがいます");
}
Any(predicate)なら、条件を満たす要素が見つかった時点で判定できます。
5-4. 空チェックで使うべき書き方
空チェックの基本形は次のとおりです。
C#if (!items.Any())
{
Console.WriteLine("空です");
}
ただし、対象がList<T>だと分かっている場合は、Countプロパティでも問題ありません。
C#if (list.Count == 0)
{
Console.WriteLine("空です");
}
配列ならLengthを使います。
C#if (array.Length == 0)
{
Console.WriteLine("空です");
}
つまり、型が具体的に分かっているならその型のプロパティを使い、IEnumerable<T>として扱うならAny()を使う、という考え方が自然です。
5-5. Count()を使うべきケースとAny()を使うべきケースの比較表
| 目的 | 推奨 | 理由 |
|---|---|---|
| 要素が存在するか知りたい | Any() | 1件見つかればよい |
| 条件を満たす要素が存在するか知りたい | Any(predicate) | 条件一致で短絡できる |
| 正確な件数が必要 | Count() | 件数そのものが必要 |
List<T>の件数が必要 | Count | プロパティで明確 |
| 配列の件数が必要 | Length | 配列の標準プロパティ |
件数がintを超える可能性がある | LongCount() | Count()はintを返す |
Count()の戻り値はintです。結果がInt32.MaxValueを超える可能性がある場合は、LongCount()を使うべきとされています。Microsoft Learn
6. Countプロパティ・Count()・Lengthの正しい選び方
6-1. List<T>ならCountプロパティを使う
List<T>の件数を取得するなら、基本はCountプロパティを使います。
C#var users = new List<string> { "Alice", "Bob" };
int count = users.Count;
users.Count()でも動くことはありますが、LINQの拡張メソッドを呼ぶ必要がありません。
C#// 動くが、List<T>ならCountプロパティでよい
int count = users.Count();
コードレビューでも、List<T>に対してCount()を使っていると「なぜLINQを使っているのか」と疑問を持たれる場合があります。可読性の面でもCountプロパティが適切です。
6-2. 配列ならLengthを使う
配列の要素数はLengthで取得します。
C#int[] numbers = { 1, 2, 3 };
int length = numbers.Length;
配列にもLINQのCount()は使えます。
C#int count = numbers.Count();
しかし、配列だと分かっているならLengthを使うほうが自然です。
C#if (numbers.Length > 0)
{
Console.WriteLine("要素があります");
}
6-3. IEnumerable<T>しか受け取れない場合の考え方
メソッドの引数がIEnumerable<T>の場合、呼び出し側から何が渡されるか分かりません。
C#void Process(IEnumerable<int> numbers)
{
if (numbers.Count() > 0)
{
foreach (var number in numbers)
{
Console.WriteLine(number);
}
}
}
このコードには2つの問題があります。
1つ目は、存在確認にCount() > 0を使っていることです。2つ目は、Count()とforeachで複数回列挙している可能性があることです。
存在確認だけなら、まずAny()にできます。
C#void Process(IEnumerable<int> numbers)
{
if (numbers.Any())
{
foreach (var number in numbers)
{
Console.WriteLine(number);
}
}
}
ただし、これでもAny()で1回、foreachでもう1回列挙する可能性があります。副作用のあるシーケンスや一度しか列挙できないシーケンスでは問題になります。
その場合は、最初にリスト化するか、処理の設計を見直します。
C#void Process(IEnumerable<int> numbers)
{
var list = numbers.ToList();
if (list.Count == 0)
{
return;
}
foreach (var number in list)
{
Console.WriteLine(number);
}
}
6-4. IReadOnlyCollection<T>やICollection<T>で受け取る設計も検討する
メソッド内で要素数が必要で、かつ複数回列挙するなら、引数の型をIEnumerable<T>にしないほうがよい場合があります。
C#void PrintUsers(IReadOnlyCollection<User> users)
{
Console.WriteLine($"ユーザー数: {users.Count}");
foreach (var user in users)
{
Console.WriteLine(user.Name);
}
}
IReadOnlyCollection<T>なら、読み取り専用のコレクションとして扱いつつ、Countプロパティを利用できます。
変更操作が必要ならICollection<T>を使う選択肢もあります。
C#void AddDefaultUser(ICollection<User> users)
{
if (users.Count == 0)
{
users.Add(new User { Name = "Default" });
}
}
引数の型は、メソッドが必要とする機能に合わせて選ぶのが基本です。
6-5. パフォーマンスを意識したメソッド引数の型設計
IEnumerable<T>は柔軟ですが、柔軟すぎるためにパフォーマンス上の前提が曖昧になります。
たとえば、次のメソッドは一見汎用的です。
C#void Export(IEnumerable<Order> orders)
{
Console.WriteLine($"件数: {orders.Count()}");
foreach (var order in orders)
{
Write(order);
}
}
しかし、このメソッドは件数取得と書き出しで複数回列挙する可能性があります。
件数が必要な仕様なら、次のようにするほうが安全です。
C#void Export(IReadOnlyCollection<Order> orders)
{
Console.WriteLine($"件数: {orders.Count}");
foreach (var order in orders)
{
Write(order);
}
}
もしくは、件数表示をやめて1回の列挙で処理する設計にします。
C#void Export(IEnumerable<Order> orders)
{
int count = 0;
foreach (var order in orders)
{
Write(order);
count++;
}
Console.WriteLine($"件数: {count}");
}
このように、IEnumerable<T>を使うか、IReadOnlyCollection<T>を使うかは、処理内容によって決めるべきです。
7. 実例コードで見るIEnumerableのCount()
7-1. List<T>に対するCount()とCountプロパティの例
まずはList<T>の例です。
C#var numbers = new List<int> { 10, 20, 30 };
// 推奨
Console.WriteLine(numbers.Count);
// 動くが、List<T>なら通常は不要
Console.WriteLine(numbers.Count());
List<T>の場合、件数はCountプロパティで取得できます。Count()を使うとLINQの拡張メソッド呼び出しになりますが、コードの意図としてはCountプロパティのほうが明確です。
変数の型がIEnumerable<T>になっている場合は、Countプロパティに直接アクセスできません。
C#IEnumerable<int> numbers = new List<int> { 10, 20, 30 };
Console.WriteLine(numbers.Count());
このような場合はCount()を使います。ただし、実体が常にList<T>だと分かっているなら、そもそもの変数や引数の型を見直してもよいでしょう。
7-2. yield returnに対するCount()の例
次に、yield returnの例です。
C#static IEnumerable<int> GetNumbers()
{
Console.WriteLine("1を返します");
yield return 1;
Console.WriteLine("2を返します");
yield return 2;
Console.WriteLine("3を返します");
yield return 3;
}
int count = GetNumbers().Count();
Console.WriteLine($"件数: {count}");
このコードでは、Count()を呼んだタイミングでGetNumbers()の中身が実行されます。件数を数えるだけでも、各yield returnまで処理が進みます。
Count()の後にもう一度列挙すると、再び実行されます。
C#var numbers = GetNumbers();
int count = numbers.Count();
foreach (var number in numbers)
{
Console.WriteLine(number);
}
このようなコードでは、同じシーケンスを複数回列挙している点に注意が必要です。
7-3. Where後のCount()とAny()の比較
条件に合う件数が必要ならCount()を使います。
C#var users = new List<User>
{
new User { Name = "Alice", IsActive = true },
new User { Name = "Bob", IsActive = false },
new User { Name = "Charlie", IsActive = true }
};
int activeCount = users.Count(user => user.IsActive);
Console.WriteLine(activeCount);
一方で、条件に合う要素が存在するかだけならAny()を使います。
C#bool hasActiveUser = users.Any(user => user.IsActive);
if (hasActiveUser)
{
Console.WriteLine("有効なユーザーがいます");
}
Count(predicate)は条件を満たす件数を数えるためのものです。Any(predicate)は条件を満たす要素の存在確認です。目的が違うため、存在確認にCount(predicate) > 0を使う必要はありません。
7-4. ループ内Count()を改善するコード例
次のコードは避けたい例です。
C#IEnumerable<int> numbers = GetNumbers();
for (int i = 0; i < numbers.Count(); i++)
{
Console.WriteLine(i);
}
改善例は、件数を先に変数へ入れることです。
C#IEnumerable<int> numbers = GetNumbers();
int count = numbers.Count();
for (int i = 0; i < count; i++)
{
Console.WriteLine(i);
}
ただし、インデックスで要素にアクセスしたいなら、IEnumerable<T>のままでは適していません。List<T>に変換します。
C#var list = GetNumbers().ToList();
for (int i = 0; i < list.Count; i++)
{
Console.WriteLine(list[i]);
}
さらに、単に全要素を処理したいだけなら、forではなくforeachで十分です。
C#foreach (var number in GetNumbers())
{
Console.WriteLine(number);
}
7-5. TryGetNonEnumeratedCount()を使った要素数取得の例
IEnumerable<T>を受け取る汎用メソッドで、列挙せずに件数が取れるなら使いたい場合は、TryGetNonEnumeratedCount()が便利です。
C#static void PrintCountIfAvailable<T>(IEnumerable<T> source)
{
if (source.TryGetNonEnumeratedCount(out int count))
{
Console.WriteLine($"件数: {count}");
}
else
{
Console.WriteLine("列挙しないと件数を取得できません");
}
}
使い方は次のとおりです。
C#PrintCountIfAvailable(new List<int> { 1, 2, 3 });
PrintCountIfAvailable(GetNumbers());
前者は列挙せずに件数を取得できる可能性が高く、後者は実際に列挙しないと件数が分からない可能性があります。
件数が必須ではなく、取れるときだけ表示したい場合に向いています。
8. LINQ to ObjectsとLINQ to SQL・Entity Frameworkでの違い
8-1. IEnumerableとIQueryableの違い
IEnumerable<T>とIQueryable<T>は、どちらもLINQで扱えますが、意味が大きく違います。
IEnumerable<T>は、主にメモリ上のオブジェクトを列挙するためのインターフェースです。WhereやCount()は、LINQ to Objectsとして.NET側で処理されます。
一方、IQueryable<T>は、クエリプロバイダーによって式木を解釈し、データソースに応じた形で実行されます。公式ドキュメントでも、IQueryableは式木を持ち、列挙時にはその式木が実行され、実行の意味はクエリプロバイダーに依存すると説明されています。Microsoft Learn
C#IQueryable<User> query = dbContext.Users
.Where(user => user.IsActive);
int count = query.Count();
この場合、Count()は単なるメモリ上の全件ループではなく、Entity FrameworkなどのプロバイダーによってSQLに変換される可能性があります。
8-2. Entity FrameworkのCount()はSQLのCOUNTに変換される
Entity Framework CoreでIQueryable<T>に対してCount()を呼ぶと、通常はデータベース側で件数を数えるSQLに変換されます。SQL Serverプロバイダーの関数マッピングでは、group.Count()がCOUNT(*)に対応することが示されています。Microsoft Learn
C#int count = await dbContext.Users
.Where(user => user.IsActive)
.CountAsync();
このようなコードでは、アプリケーション側に全ユーザーを読み込んでから数えるのではなく、データベース側で件数を集計する形になりやすいです。
ただし、途中でAsEnumerable()を呼ぶと、それ以降はLINQ to Objectsとして処理されます。
C#int count = dbContext.Users
.AsEnumerable()
.Where(user => user.IsActive)
.Count();
この場合、データベースから取得した後にメモリ上で処理される可能性があるため、注意が必要です。
8-3. Any()はSQLのEXISTS相当になりやすい
DBアクセスでも、存在確認ならCount() > 0よりAny()が適切です。
C#bool exists = await dbContext.Users
.AnyAsync(user => user.IsActive);
Any()は「条件を満たす行が存在するか」を表すため、SQLではEXISTSやそれに近い効率的な形に変換されることがあります。
一方、次のようなコードは、存在確認のために件数を求める意図になります。
C#bool exists = await dbContext.Users
.CountAsync(user => user.IsActive) > 0;
DB側で最適化される場合もありますが、コードの意図としてはAny()のほうが明確です。
8-4. DBアクセスでは発行されるSQLを確認する
Entity Frameworkを使う場合、最終的にどのようなSQLが発行されるかを確認することが重要です。
LINQはC#のコードとして書けるため便利ですが、すべての書き方が同じSQLになるわけではありません。EF Coreの公式ドキュメントでも、LINQクエリはデータベースプロバイダーに渡され、データベース固有のクエリ言語に変換されると説明されています。Microsoft Learn
たとえば、次のような点を確認します。
| 確認点 | 理由 |
|---|---|
Count()がSQLのCOUNTになっているか | 全件取得していないか確認するため |
Any()が効率的なSQLになっているか | 存在確認が重くなっていないか確認するため |
AsEnumerable()を早く呼びすぎていないか | クライアント評価を避けるため |
不要なToList()がないか | 全件読み込みを避けるため |
パフォーマンスが重要な箇所では、ログやToQueryString()などを使ってSQLを確認しましょう。
8-5. メモリ上のLINQとDB向けLINQを混同しない
同じCount()でも、LINQ to ObjectsとEntity Frameworkでは意味が変わります。
C#// LINQ to Objects
IEnumerable<User> users = GetUsers();
int count1 = users.Count();
// Entity Framework
IQueryable<User> query = dbContext.Users;
int count2 = query.Count();
前者は.NET上で列挙して数える可能性があります。後者はクエリプロバイダーによってSQLに変換される可能性があります。
特に注意すべきなのは、IQueryable<T>を途中でIEnumerable<T>に変換してしまうケースです。
C#IEnumerable<User> users = dbContext.Users;
int count = users
.Where(user => user.IsActive)
.Count();
このように書くと、意図せずLINQ to Objects側で処理される可能性があります。DB向けのクエリとして処理したいなら、IQueryable<T>のまま組み立てることが重要です。
9. IEnumerableのCount()でよくある疑問
9-1. Count()を呼ぶと元のIEnumerableは消費されるのか
通常のIEnumerable<T>では、Count()を呼ぶと列挙は行われますが、元のコレクション自体が消えるわけではありません。
C#var list = new List<int> { 1, 2, 3 };
int count = list.Count();
foreach (var item in list)
{
Console.WriteLine(item);
}
このコードは問題なく動きます。
ただし、注意が必要なのは「再列挙できるとは限らない」シーケンスです。たとえば、外部リソース、ストリーム、状態を持つカスタムIEnumerable<T>などでは、Count()による列挙が後続処理に影響する可能性があります。
また、yield returnの中で副作用がある場合、Count()を呼ぶだけでその副作用が発生します。
C#static IEnumerable<int> GetNumbers()
{
Console.WriteLine("列挙されました");
yield return 1;
}
Count()は安全なメタ情報取得ではなく、列挙を伴う可能性のある処理として考えるべきです。
9-2. Count()とLongCount()の違いは何か
Count()はintを返します。
C#int count = items.Count();
一方、LongCount()はlongを返します。
C#long count = items.LongCount();
通常のアプリケーションではCount()で十分なことが多いですが、件数がint.MaxValueを超える可能性がある巨大なシーケンスではLongCount()を検討します。公式ドキュメントでも、結果がMaxValueより大きくなる可能性を想定する場合はLongCountを使うよう説明されています。Microsoft Learn
ただし、LongCount()も対象によっては列挙が必要です。戻り値の型が大きいだけで、常に高速になるわけではありません。
9-3. Count()とToList().Countはどちらがよいのか
単に件数だけが必要なら、通常はCount()で十分です。
C#int count = items.Count();
ToList().Countは、全要素をリストに格納してから件数を取得します。
C#int count = items.ToList().Count;
これは、件数を知るだけなら余計なメモリ確保になることがあります。
ただし、次のように後続で何度も列挙する場合は、ToList()が有効なこともあります。
C#var list = items.ToList();
Console.WriteLine(list.Count);
foreach (var item in list)
{
Console.WriteLine(item);
}
判断基準は次のとおりです。
| 目的 | 推奨 |
|---|---|
| 件数だけ知りたい | Count() |
| 存在確認だけしたい | Any() |
| 後で何度も使う | ToList()してCount |
| 1回だけ順番に処理する | foreach |
| メモリ使用量を抑えたい | 不要なToList()を避ける |
9-4. 複数回列挙を避けるにはどうすればよいか
複数回列挙を避ける方法はいくつかあります。
まず、件数と処理を1回のループで済ませる方法です。
C#int count = 0;
foreach (var item in items)
{
Process(item);
count++;
}
Console.WriteLine($"処理件数: {count}");
次に、必要に応じてリスト化する方法です。
C#var list = items.ToList();
Console.WriteLine(list.Count);
foreach (var item in list)
{
Process(item);
}
また、そもそもメソッドの引数をIReadOnlyCollection<T>にする方法もあります。
C#void ProcessAll(IReadOnlyCollection<Item> items)
{
Console.WriteLine(items.Count);
foreach (var item in items)
{
Process(item);
}
}
IEnumerable<T>は便利ですが、「何度も列挙する」「件数が必要」「インデックスアクセスが必要」といった要件があるなら、別の型を選ぶほうが自然です。
9-5. nullのIEnumerableにCount()を使うとどうなるか
nullのIEnumerable<T>にCount()を呼ぶと例外になります。
C#IEnumerable<int>? numbers = null;
int count = numbers.Count(); // 例外
nullの可能性がある場合は、null条件演算子を使います。
C#int count = numbers?.Count() ?? 0;
または、空のシーケンスに置き換えます。
C#IEnumerable<int> safeNumbers = numbers ?? Enumerable.Empty<int>();
int count = safeNumbers.Count();
ただし、設計としては、可能であればnullではなく空のコレクションを返すほうが扱いやすくなります。
C#IEnumerable<int> GetNumbers()
{
return Enumerable.Empty<int>();
}
呼び出し側が毎回nullチェックをしなくて済むため、コードもシンプルになります。
まとめ
C#のIEnumerableに対するCount()は、常に遅いわけではありません。List<T>やHashSet<T>のように要素数を保持しているコレクションなら、効率よく件数を取得できる場合があります。一方、yield returnで生成されたシーケンス、LINQのWhereやSelectを重ねたクエリ、ファイルやAPIなど外部リソースを扱うシーケンスでは、Count()が全件列挙を引き起こすことがあります。
基本的な使い分けは次のとおりです。
| やりたいこと | 使うもの |
|---|---|
| 配列の要素数を知りたい | Length |
List<T>の要素数を知りたい | Count |
ICollection<T>の要素数を知りたい | Count |
IEnumerable<T>の正確な件数が必要 | Count() |
| 要素が存在するか知りたい | Any() |
| 条件に合う要素が存在するか知りたい | Any(predicate) |
| 条件に合う件数が必要 | Count(predicate) |
| 列挙せずに件数取得できるか確認したい | TryGetNonEnumeratedCount() |
特に、存在確認でCount() > 0を書くのは避け、Any()を使うのが基本です。Count()は「件数そのものが必要なとき」に使い、空チェックや条件一致の確認にはAny()を使いましょう。
また、メソッド内で件数が必要なら、引数をIEnumerable<T>ではなくIReadOnlyCollection<T>やICollection<T>にする設計も検討できます。IEnumerable<T>は柔軟ですが、要素数を直接持つとは限りません。
Count()、Countプロパティ、Length、Any()を正しく使い分けることで、C#のLINQコードは読みやすくなり、不要な列挙やパフォーマンス低下も避けやすくなります。

