LINQ

【C#】LINQメソッド構文の使い方とサンプル集:フィルタリングから集計まで

C#のLINQメソッド構文は、WhereSelectなどの拡張メソッドをチェーンでつなぎ、IEnumerable<T>IQueryable<T>のデータを宣言的に操作できる仕組みです。

SQL風のクエリ構文より柔軟で、ラムダ式を組み合わせることで条件や変換を動的に組み立てやすく、型安全に集計・並べ替え・結合などが行えます。

目次から探す
  1. メソッド構文の基本
  2. フィルタリング演算子
  3. 投影演算子
  4. 並べ替え演算子
  5. 集計演算子
  6. グループ化演算子
  7. 結合演算子
  8. 分割演算子
  9. セット演算子
  10. 生成演算子
  11. 変換演算子
  12. 要素演算子
  13. 量的判定演算子
  14. 異常処理とクエリの安全性
  15. パフォーマンス最適化
  16. 実践シナリオ別サンプル
  17. カスタム拡張メソッドの作成
  18. 非同期LINQの活用
  19. Queryable対応の注意点
  20. よくある落とし穴と対策
  21. まとめ

メソッド構文の基本

LINQのメソッド構文は、C#の拡張メソッドを活用して、コレクションに対して連続的に操作を適用していくスタイルです。

ここでは、メソッド構文の基礎となる拡張メソッドの仕組みや、ラムダ式の使い方、データの流れ、そして遅延実行と即時実行の違いについて詳しく解説します。

拡張メソッドのしくみ

拡張メソッドは、既存のクラスやインターフェースに対して、新たなメソッドを追加できるC#の機能です。

LINQのメソッド構文は、この拡張メソッドを利用してIEnumerable<T>IQueryable<T>に対して標準クエリ演算子を提供しています。

拡張メソッドは、静的クラスの静的メソッドとして定義され、第一引数にthisキーワードを付けることで、対象の型のインスタンスメソッドのように呼び出せます。

例えば、Whereメソッドは以下のように定義されています。

public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)

この定義により、IEnumerable<T>のインスタンスに対してWhereメソッドを呼び出すことが可能です。

拡張メソッドのメリットは、元のクラスを変更せずに機能を追加できる点にあります。

LINQでは、この仕組みを活用して、データのフィルタリングや変換、集計などの操作をメソッドチェーンで記述できるようにしています。

ラムダ式と匿名メソッド

LINQのメソッド構文では、条件や変換のロジックを引数として渡すために、ラムダ式や匿名メソッドを多用します。

ラムダ式は、無名関数を簡潔に記述できる構文で、Func<T, TResult>Action<T>などのデリゲート型にマッチします。

例えば、Whereメソッドの条件はFunc<TSource, bool>型のデリゲートで受け取ります。

これをラムダ式で書くと以下のようになります。

numbers.Where(n => n % 2 == 0)

ここでn => n % 2 == 0は、引数nを受け取り、nが偶数かどうかを判定する無名関数です。

ラムダ式は、引数の型推論が可能で、コードが非常にシンプルになります。

匿名メソッドは、ラムダ式が登場する前に使われていた無名関数の記述方法で、以下のように書きます。

numbers.Where(delegate(int n) { return n % 2 == 0; })

現在はラムダ式が主流ですが、匿名メソッドも同様に使えます。

データシーケンスの流れ

LINQのメソッド構文では、データはシーケンスIEnumerable<T>IQueryable<T>として扱われます。

これらのシーケンスは、要素の集合を表し、メソッドチェーンで操作を連結していくことで、最終的な結果を得ます。

例えば、以下のコードを考えてみましょう。

var result = numbers
    .Where(n => n > 3)
    .Select(n => n * 2);

この場合、numbersという元のシーケンスから、Whereで条件に合う要素だけを抽出し、Selectで各要素を2倍に変換しています。

ここで重要なのは、これらの操作は「データの流れ」を表現しているだけで、実際の処理はまだ実行されていないことが多い点です。

LINQのシーケンスは、操作を連結して新しいシーケンスを返すため、元のデータは変更されません。

これにより、複数の操作を組み合わせて柔軟にデータを処理できます。

遅延実行と即時実行

LINQのメソッド構文で重要な概念に「遅延実行」と「即時実行」があります。

これらは、クエリの実行タイミングに関する違いです。

遅延実行

遅延実行は、クエリの定義時には実際のデータ処理を行わず、結果が必要になった時点で初めて処理を実行する方式です。

WhereSelectなどの多くの標準クエリ演算子は遅延実行を採用しています。

例えば、以下のコードは遅延実行の例です。

var query = numbers.Where(n => n > 3).Select(n => n * 2);
// この時点ではまだ処理は実行されていない
foreach (var item in query)
{
    Console.WriteLine(item);
}
// ここで初めて処理が実行される

遅延実行のメリットは、不要な処理を避けられることや、データの変更を反映できることです。

例えば、queryを定義した後にnumbersの内容が変わっていれば、foreachで処理した結果も変わります。

即時実行

即時実行は、クエリを定義した時点で処理を実行し、結果をメモリ上に確保する方式です。

ToList(), ToArray(), Count(), Sum()などのメソッドは即時実行を行います。

例えば、以下のコードは即時実行の例です。

var list = numbers.Where(n => n > 3).ToList();
// この時点で処理が実行され、結果がリストに格納される

即時実行のメリットは、結果を固定化できることや、複数回の列挙で同じ結果を得られることです。

ただし、大量のデータを即時に処理するとパフォーマンスに影響が出る場合があります。

遅延実行と即時実行の使い分け

LINQのメソッド構文では、遅延実行と即時実行を適切に使い分けることが重要です。

一般的には、複数の操作を組み合わせてクエリを定義し、最後にToList()ToArray()で即時実行して結果を取得するパターンが多いです。

また、デバッグ時には即時実行を使って中間結果を確認することもあります。

遅延実行の特性を理解しておくと、効率的で柔軟なデータ処理が可能になります。

フィルタリング演算子

Where

Whereメソッドは、LINQの中でも最も基本的なフィルタリング演算子です。

指定した条件に合致する要素だけを抽出し、新しいシーケンスとして返します。

WhereIEnumerable<T>の拡張メソッドであり、引数には要素を判定するための述語Func<T, bool>を受け取ります。

複数条件の組み合わせ

複数の条件を組み合わせてフィルタリングしたい場合、Whereの中で論理演算子を使う方法が一般的です。

例えば、数値のリストから偶数かつ3より大きい値を抽出する場合は以下のように記述します。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8 };
        // 偶数かつ3より大きい数を抽出
        var filtered = numbers.Where(n => n % 2 == 0 && n > 3);
        foreach (var num in filtered)
        {
            Console.WriteLine(num);
        }
    }
}
4
6
8

この例では、n % 2 == 0で偶数判定、n > 3で3より大きいかどうかを判定し、両方の条件を&&で結合しています。

Whereの中で複数条件を組み合わせることで、柔軟なフィルタリングが可能です。

また、条件が複雑になる場合は、条件式を別のメソッドに切り出して可読性を高めることもできます。

static bool IsEvenAndGreaterThanThree(int n)
{
    return n % 2 == 0 && n > 3;
}
var filtered = numbers.Where(IsEvenAndGreaterThanThree);

インデックス付き条件

Whereメソッドには、要素の値だけでなく、そのインデックス(位置)を利用して条件を指定できるオーバーロードがあります。

これは、Func<TSource, int, bool>型の述語を受け取り、要素の値とインデックスの両方を判定に使えます。

例えば、偶数番目(0始まりのインデックス)にある要素だけを抽出する場合は以下のように書きます。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var fruits = new List<string> { "apple", "banana", "cherry", "date", "elderberry" };
        // インデックスが偶数の要素を抽出
        var filtered = fruits.Where((fruit, index) => index % 2 == 0);
        foreach (var fruit in filtered)
        {
            Console.WriteLine(fruit);
        }
    }
}
apple
cherry
elderberry

この例では、Whereの述語に(fruit, index)という2つの引数を受け取るラムダ式を渡しています。

index % 2 == 0の条件で偶数インデックスの要素だけを抽出しています。

インデックス付きのWhereは、要素の位置に基づくフィルタリングが必要な場合に便利です。

例えば、リストの奇数番目だけを処理したい場合や、特定の範囲のインデックスを抽出したい場合などに活用できます。

投影演算子

Select

Selectメソッドは、LINQの投影演算子の代表で、シーケンスの各要素を別の形に変換して新しいシーケンスを作成します。

引数には変換処理を行うラムダ式を渡し、元の要素から新しい要素を生成します。

型変換のパターン

Selectを使うと、要素の型を別の型に変換することが簡単にできます。

例えば、整数のリストから文字列のリストを作成する場合は以下のように記述します。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var numbers = new List<int> { 1, 2, 3, 4 };
        // int型からstring型に変換
        var strings = numbers.Select(n => n.ToString());
        foreach (var s in strings)
        {
            Console.WriteLine(s);
        }
    }
}
1
2
3
4

この例では、Selectのラムダ式でn.ToString()を呼び出し、整数を文字列に変換しています。

Selectは元のシーケンスの要素数を変えずに、各要素を変換して新しいシーケンスを返します。

また、複雑な型変換も可能です。

例えば、以下のようにクラスのインスタンスを別のクラスのインスタンスに変換することもできます。

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
class PersonDto
{
    public string DisplayName { get; set; }
}
var people = new List<Person>
{
    new Person { Name = "Alice", Age = 30 },
    new Person { Name = "Bob", Age = 25 }
};
var dtos = people.Select(p => new PersonDto { DisplayName = p.Name + " (" + p.Age + ")" });
foreach (var dto in dtos)
{
    Console.WriteLine(dto.DisplayName);
}
Alice (30)
Bob (25)

このように、Selectは型変換だけでなく、要素の内容を自由に加工して新しいシーケンスを作るのに適しています。

匿名型の生成

Selectは匿名型を生成するのにもよく使われます。

匿名型は名前のないクラスで、複数のプロパティをまとめて一時的なデータ構造として扱えます。

例えば、以下のように複数のプロパティを持つ匿名型を作成できます。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var people = new List<Person>
        {
            new Person { Name = "Alice", Age = 30 },
            new Person { Name = "Bob", Age = 25 }
        };
        // 匿名型で名前と年齢の2つのプロパティを持つシーケンスを作成
        var projections = people.Select(p => new { p.Name, p.Age });
        foreach (var item in projections)
        {
            Console.WriteLine($"Name: {item.Name}, Age: {item.Age}");
        }
    }
}
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
Name: Alice, Age: 30
Name: Bob, Age: 25

匿名型は型名を明示しなくても複数の値をまとめられるため、簡潔にデータを扱いたい場合に便利です。

ただし、匿名型はメソッドの戻り値として返すことができないため、メソッド内での一時的な利用に向いています。

SelectMany

SelectManyは、多階層のコレクションを平坦化(フラット化)するための演算子です。

例えば、リストの中にリストがあるような構造を一つのシーケンスにまとめたい場合に使います。

多階層コレクションの平坦化

例えば、複数のクラスがそれぞれ複数の趣味を持っている場合、各クラスの趣味リストを一つのシーケンスにまとめるにはSelectManyを使います。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var people = new List<Person>
        {
            new Person { Name = "Alice", Hobbies = new List<string> { "Reading", "Swimming" } },
            new Person { Name = "Bob", Hobbies = new List<string> { "Cycling", "Cooking" } }
        };
        // 各人の趣味を一つのシーケンスにまとめる
        var allHobbies = people.SelectMany(p => p.Hobbies);
        foreach (var hobby in allHobbies)
        {
            Console.WriteLine(hobby);
        }
    }
}
class Person
{
    public string Name { get; set; }
    public List<string> Hobbies { get; set; }
}
Reading
Swimming
Cycling
Cooking

この例では、peopleの各要素が持つHobbiesリストをSelectManyで展開し、すべての趣味を一つのシーケンスにまとめています。

Selectを使うと趣味のリストのリストIEnumerable<IEnumerable<string>>になりますが、SelectManyはそれを平坦化してIEnumerable<string>にします。

SelectManyは、ネストされたコレクションを扱う際に非常に便利で、複雑なデータ構造をシンプルに処理できます。

さらに、SelectManyは複数のシーケンスを結合する際の結合条件を指定するオーバーロードもありますが、基本的な使い方は上記の通りです。

並べ替え演算子

OrderBy

OrderByメソッドは、シーケンスの要素を指定したキーに基づいて昇順に並べ替えます。

引数には、並べ替えの基準となるキーを抽出するためのラムダ式(キーセレクタ)を渡します。

キーセレクタは、各要素から比較対象となる値を返す関数です。

キーセレクタの指定

キーセレクタは、Func<TSource, TKey>型のデリゲートで、要素から並べ替えに使うキーを抽出します。

例えば、文字列のリストを文字列の長さで昇順に並べ替える場合は以下のように書きます。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var words = new List<string> { "apple", "banana", "cherry", "date" };
        // 文字列の長さで昇順に並べ替え
        var ordered = words.OrderBy(word => word.Length);
        foreach (var word in ordered)
        {
            Console.WriteLine(word);
        }
    }
}
date
apple
banana
cherry

この例では、word => word.Lengthがキーセレクタで、各文字列の長さを取得して並べ替えの基準にしています。

OrderByは元のシーケンスを変更せず、新しい並べ替え済みのシーケンスを返します。

OrderByDescending

OrderByDescendingは、OrderByと同様にキーセレクタを指定しますが、並べ替えの順序が降順(大きい値から小さい値)になります。

例えば、先ほどの文字列の長さで降順に並べ替える場合は以下のように書きます。

var orderedDesc = words.OrderByDescending(word => word.Length);
foreach (var word in orderedDesc)
{
    Console.WriteLine(word);
}
banana
cherry
apple
date

降順に並べ替えたい場合はOrderByDescendingを使うと簡潔に記述できます。

ThenBy / ThenByDescending

ThenByThenByDescendingは、OrderByOrderByDescendingで並べ替えた結果に対して、さらに副次的なキーで並べ替えを行うためのメソッドです。

複数のキーを使った複合的な並べ替えを実現します。

複合キーでの並べ替え

例えば、人物のリストを「年齢の昇順」で並べ替え、同じ年齢の人が複数いる場合は「名前の昇順」で並べ替えたい場合は以下のように書きます。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var people = new List<Person>
        {
            new Person { Name = "Alice", Age = 30 },
            new Person { Name = "Bob", Age = 25 },
            new Person { Name = "Charlie", Age = 30 },
            new Person { Name = "David", Age = 25 }
        };
        // 年齢で昇順に並べ替え、同じ年齢の場合は名前で昇順に並べ替え
        var ordered = people.OrderBy(p => p.Age).ThenBy(p => p.Name);
        foreach (var person in ordered)
        {
            Console.WriteLine($"{person.Name} ({person.Age})");
        }
    }
}
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
Bob (25)
David (25)
Alice (30)
Charlie (30)

この例では、まずOrderBy(p => p.Age)で年齢の昇順に並べ替え、次にThenBy(p => p.Name)で同じ年齢の人を名前の昇順で並べ替えています。

ThenByDescendingを使うと副次キーの降順並べ替えも可能です。

Reverse

Reverseメソッドは、シーケンスの要素の順序を単純に逆転させます。

並べ替えとは異なり、要素の値を比較して順序を決めるのではなく、現在の順序を反転させるだけです。

例えば、以下のように使います。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var numbers = new List<int> { 1, 2, 3, 4, 5 };
        // 順序を逆にする
        var reversed = numbers.Reverse();
        foreach (var num in reversed)
        {
            Console.WriteLine(num);
        }
    }
}
5
4
3
2
1

Reverseは元のシーケンスの順序を反転した新しいシーケンスを返します。

注意点として、OrderByOrderByDescendingのようにキーを指定して並べ替えるのではなく、単純に要素の並びを逆にするだけなので、並べ替えの前後で使い分ける必要があります。

集計演算子

Count

Countメソッドは、シーケンス内の要素数を取得します。

引数なしで呼び出すと、シーケンスの全要素数を返します。

また、条件を指定するオーバーロードもあり、条件に合致する要素の数を数えることも可能です。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
        // 全要素数を取得
        int totalCount = numbers.Count();
        // 偶数の要素数を取得
        int evenCount = numbers.Count(n => n % 2 == 0);
        Console.WriteLine($"全要素数: {totalCount}");
        Console.WriteLine($"偶数の要素数: {evenCount}");
    }
}
全要素数: 6
偶数の要素数: 3

Countは即時実行され、シーケンスを列挙して要素数を計算します。

Sum

Sumメソッドは、数値シーケンスの合計値を計算します。

整数、浮動小数点数、decimalなどの数値型に対応しています。

要素が数値型でない場合は、変換用のラムダ式を渡して合計を計算できます。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var numbers = new List<int> { 1, 2, 3, 4, 5 };
        // 合計値を計算
        int total = numbers.Sum();
        Console.WriteLine($"合計値: {total}");
    }
}
合計値: 15

クラスのリストから特定の数値プロパティの合計を計算する例もあります。

class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}
var products = new List<Product>
{
    new Product { Name = "A", Price = 100m },
    new Product { Name = "B", Price = 200m },
    new Product { Name = "C", Price = 150m }
};
decimal totalPrice = products.Sum(p => p.Price);
Console.WriteLine($"合計金額: {totalPrice}");
合計金額: 450

Average

Averageメソッドは、数値シーケンスの平均値を計算します。

Sumと同様に、数値型のシーケンスに対して使えます。

要素が数値型でない場合は、変換用のラムダ式を渡します。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var scores = new List<double> { 80.5, 90.0, 75.5, 88.0 };
        // 平均値を計算
        double average = scores.Average();
        Console.WriteLine($"平均点: {average}");
    }
}
平均点: 83.5

クラスのプロパティの平均を計算する例も同様です。

class Student
{
    public string Name { get; set; }
    public int Score { get; set; }
}
var students = new List<Student>
{
    new Student { Name = "Tom", Score = 85 },
    new Student { Name = "Jane", Score = 90 },
    new Student { Name = "Bob", Score = 78 }
};
double avgScore = students.Average(s => s.Score);
Console.WriteLine($"平均スコア: {avgScore}");
平均スコア: 84.3333333333333

Min / Max

MinMaxメソッドは、シーケンス内の最小値と最大値を取得します。

数値型だけでなく、文字列や日付型など比較可能な型にも使えます。

ラムダ式を渡して特定のプロパティの最小・最大を取得することも可能です。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var numbers = new List<int> { 10, 5, 20, 15 };
        int minValue = numbers.Min();
        int maxValue = numbers.Max();
        Console.WriteLine($"最小値: {minValue}");
        Console.WriteLine($"最大値: {maxValue}");
    }
}
最小値: 5
最大値: 20

クラスのプロパティでの例です。

class Employee
{
    public string Name { get; set; }
    public int Salary { get; set; }
}
var employees = new List<Employee>
{
    new Employee { Name = "Alice", Salary = 50000 },
    new Employee { Name = "Bob", Salary = 60000 },
    new Employee { Name = "Charlie", Salary = 55000 }
};
int minSalary = employees.Min(e => e.Salary);
int maxSalary = employees.Max(e => e.Salary);
Console.WriteLine($"最低給与: {minSalary}");
Console.WriteLine($"最高給与: {maxSalary}");
最低給与: 50000
最高給与: 60000

Aggregate

Aggregateメソッドは、シーケンスの要素を集約して単一の値を生成するための汎用的な演算子です。

初期値と集約関数を指定して、要素を順に処理しながら結果を作り上げます。

SumCountなどの集計は内部的にAggregateを使って実装されています。

カスタム集計の実装

例えば、文字列のリストをカンマ区切りの一つの文字列にまとめる処理をAggregateで実装できます。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var words = new List<string> { "apple", "banana", "cherry" };
        // カンマ区切りの文字列に集約
        string result = words.Aggregate((acc, word) => acc + ", " + word);
        Console.WriteLine(result);
    }
}
apple, banana, cherry

この例では、Aggregateのラムダ式で累積値accに現在の要素wordをカンマと空白で連結しています。

初期値を指定しない場合、最初の要素が初期値として使われます。

初期値を指定して安全に集約する例もあります。

string result = words.Aggregate("Fruits: ", (acc, word) => acc + word + "; ");
Console.WriteLine(result);
Fruits: apple; banana; cherry;

また、数値の積を計算するカスタム集計も可能です。

var numbers = new List<int> { 2, 3, 4 };
int product = numbers.Aggregate(1, (acc, n) => acc * n);
Console.WriteLine($"積: {product}");
積: 24

Aggregateは柔軟に集計処理をカスタマイズできるため、標準の集計メソッドでは対応できない複雑な処理に適しています。

グループ化演算子

GroupBy

GroupByメソッドは、シーケンスの要素を指定したキーに基づいてグループ化し、キーとグループ化された要素のコレクションを持つシーケンスを返します。

グループ化の結果はIEnumerable<IGrouping<TKey, TElement>>型となり、各IGroupingはキーとそのキーに属する要素の列を表します。

単一キーのグループ化

単一のキーでグループ化する場合、キーセレクタに要素のプロパティや値を指定します。

例えば、文字列のリストを先頭の文字でグループ化する例です。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var words = new List<string> { "apple", "apricot", "banana", "blueberry", "cherry" };
        // 先頭の文字でグループ化
        var groups = words.GroupBy(word => word[0]);
        foreach (var group in groups)
        {
            Console.WriteLine($"キー: {group.Key}");
            foreach (var word in group)
            {
                Console.WriteLine($"  {word}");
            }
        }
    }
}
キー: a
  apple
  apricot
キー: b
  banana
  blueberry
キー: c
  cherry

この例では、word => word[0]がキーセレクタで、単語の先頭文字でグループ化しています。

GroupByの戻り値は、キーとそのキーに属する要素の列を持つグループのシーケンスです。

複合キーのグループ化

複数のプロパティを組み合わせてグループ化したい場合は、匿名型などを使って複合キーを作成します。

例えば、人物のリストを「年齢」と「性別」の組み合わせでグループ化する例です。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var people = new List<Person>
        {
            new Person { Name = "Alice", Age = 30, Gender = "Female" },
            new Person { Name = "Bob", Age = 25, Gender = "Male" },
            new Person { Name = "Carol", Age = 30, Gender = "Female" },
            new Person { Name = "Dave", Age = 25, Gender = "Male" },
            new Person { Name = "Eve", Age = 30, Gender = "Male" }
        };
        // 年齢と性別で複合キーのグループ化
        var groups = people.GroupBy(p => new { p.Age, p.Gender });
        foreach (var group in groups)
        {
            Console.WriteLine($"キー: Age={group.Key.Age}, Gender={group.Key.Gender}");
            foreach (var person in group)
            {
                Console.WriteLine($"  {person.Name}");
            }
        }
    }
}
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Gender { get; set; }
}
キー: Age=30, Gender=Female
  Alice
  Carol
キー: Age=25, Gender=Male
  Bob
  Dave
キー: Age=30, Gender=Male
  Eve

匿名型をキーに使うことで、複数のプロパティを組み合わせたグループ化が簡単に実現できます。

匿名型は値の比較がプロパティ単位で行われるため、同じ値の組み合わせが同じグループにまとめられます。

ToLookup

ToLookupメソッドは、GroupByと似ていますが、結果をILookup<TKey, TElement>型のコレクションとして返します。

ILookupはキーと要素のグループを保持し、キーを指定して高速に要素の列を取得できる辞書のような構造です。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var words = new List<string> { "apple", "apricot", "banana", "blueberry", "cherry" };
        // 先頭文字でLookupを作成
        var lookup = words.ToLookup(word => word[0]);
        // キー'b'の要素を取得
        var bWords = lookup['b'];
        Console.WriteLine("キー 'b' の単語:");
        foreach (var word in bWords)
        {
            Console.WriteLine(word);
        }
    }
}
キー 'b' の単語:
banana
blueberry

ToLookupは、キーでの高速な検索が必要な場合に便利です。

GroupByは遅延実行されますが、ToLookupは即時実行されて結果をメモリに保持します。

複数回キーでアクセスする場合はToLookupの方が効率的です。

結合演算子

Join

Joinメソッドは、2つのシーケンスを指定したキーで内部結合(inner join)し、共通のキーを持つ要素の組み合わせを生成します。

SQLの内部結合に相当し、両方のシーケンスに存在するキーの要素だけが結合されます。

内部結合

例えば、社員リストと部署リストを部署IDで結合し、社員名と部署名のペアを作成する例です。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var employees = new List<Employee>
        {
            new Employee { Id = 1, Name = "Alice", DepartmentId = 1 },
            new Employee { Id = 2, Name = "Bob", DepartmentId = 2 },
            new Employee { Id = 3, Name = "Charlie", DepartmentId = 3 },
            new Employee { Id = 4, Name = "David", DepartmentId = 2 }
        };
        var departments = new List<Department>
        {
            new Department { Id = 1, Name = "Sales" },
            new Department { Id = 2, Name = "HR" }
        };
        // 部署IDで内部結合し、社員名と部署名のペアを作成
        var query = employees.Join(
            departments,
            emp => emp.DepartmentId,    // 外側シーケンスのキーセレクタ
            dept => dept.Id,            // 内側シーケンスのキーセレクタ
            (emp, dept) => new          // 結合結果の選択
            {
                EmployeeName = emp.Name,
                DepartmentName = dept.Name
            });
        foreach (var item in query)
        {
            Console.WriteLine($"{item.EmployeeName} - {item.DepartmentName}");
        }
    }
}
class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int DepartmentId { get; set; }
}
class Department
{
    public int Id { get; set; }
    public string Name { get; set; }
}
Alice - Sales
Bob - HR
David - HR

この例では、employeesdepartmentsDepartmentIdIdで結合しています。

Charlieは部署IDが3ですが、departmentsにID3の部署がないため結果に含まれません。

GroupJoin

GroupJoinは、2つのシーケンスをキーで結合し、外側の各要素に対して内側の一致する要素のグループを関連付けます。

SQLの左外部結合(left outer join)に似た動作をしますが、GroupJoin自体は左外部結合ではなく、グループ化された結合結果を返します。

以下は、社員ごとに所属する部署の情報をグループ化して表示する例です。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var employees = new List<Employee>
        {
            new Employee { Id = 1, Name = "Alice", DepartmentId = 1 },
            new Employee { Id = 2, Name = "Bob", DepartmentId = 2 },
            new Employee { Id = 3, Name = "Charlie", DepartmentId = 3 }
        };
        var departments = new List<Department>
        {
            new Department { Id = 1, Name = "Sales" },
            new Department { Id = 2, Name = "HR" }
        };
        // GroupJoinで社員ごとに部署のグループを関連付ける
        var query = departments.GroupJoin(
            employees,
            dept => dept.Id,            // 外側シーケンスのキーセレクタ
            emp => emp.DepartmentId,    // 内側シーケンスのキーセレクタ
            (dept, emps) => new         // 結果の選択
            {
                DepartmentName = dept.Name,
                Employees = emps
            });
        foreach (var group in query)
        {
            Console.WriteLine($"部署: {group.DepartmentName}");
            foreach (var emp in group.Employees)
            {
                Console.WriteLine($"  {emp.Name}");
            }
        }
    }
}
class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int DepartmentId { get; set; }
}
class Department
{
    public int Id { get; set; }
    public string Name { get; set; }
}
部署: Sales
  Alice
部署: HR
  Bob

この例では、departmentsを外側シーケンス、employeesを内側シーケンスとしてGroupJoinを行い、各部署に所属する社員のグループを取得しています。

部署ID3の部署は存在しないため、Charlieは結果に含まれません。

Zip

Zipメソッドは、2つのシーケンスの要素をペアにして結合し、新しいシーケンスを作成します。

両方のシーケンスの要素数が異なる場合は、短い方のシーケンスの長さに合わせて処理が行われます。

ペア要素の生成

例えば、2つのリストの対応する要素を組み合わせて文字列を作成する例です。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var firstNames = new List<string> { "Alice", "Bob", "Charlie" };
        var lastNames = new List<string> { "Smith", "Johnson", "Williams" };
        // 2つのリストの要素をペアにしてフルネームを作成
        var fullNames = firstNames.Zip(lastNames, (first, last) => $"{first} {last}");
        foreach (var name in fullNames)
        {
            Console.WriteLine(name);
        }
    }
}
Alice Smith
Bob Johnson
Charlie Williams

この例では、firstNameslastNamesの対応する要素を結合してフルネームを作成しています。

Zipは要素のペアを作るのに便利で、2つのシーケンスを同時に処理したい場合に使います。

分割演算子

Take / TakeLast

Takeメソッドは、シーケンスの先頭から指定した数の要素を取得します。

例えば、リストの最初の3要素だけを取得したい場合に使います。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var numbers = Enumerable.Range(1, 10).ToList();
        // 先頭から3つの要素を取得
        var firstThree = numbers.Take(3);
        foreach (var num in firstThree)
        {
            Console.WriteLine(num);
        }
    }
}
1
2
3

一方、TakeLastはシーケンスの末尾から指定した数の要素を取得します。

TakeLastは.NET Core 3.0以降で利用可能です。

var lastThree = numbers.TakeLast(3);
foreach (var num in lastThree)
{
    Console.WriteLine(num);
}
8
9
10

TakeTakeLastは、シーケンスの一部を簡単に切り出すのに便利です。

Skip / SkipLast

Skipメソッドは、シーケンスの先頭から指定した数の要素をスキップし、それ以降の要素を取得します。

例えば、最初の3要素を除いた残りを取得する場合に使います。

var skipThree = numbers.Skip(3);
foreach (var num in skipThree)
{
    Console.WriteLine(num);
}
4
5
6
7
8
9
10

SkipLastはシーケンスの末尾から指定した数の要素をスキップし、それ以外の要素を取得します。

こちらも.NET Core 3.0以降で利用可能です。

var skipLastThree = numbers.SkipLast(3);
foreach (var num in skipLastThree)
{
    Console.WriteLine(num);
}
1
2
3
4
5
6
7

SkipSkipLastは、不要な部分を除外してデータを取得したいときに役立ちます。

TakeWhile / SkipWhile

TakeWhileは、条件を満たす限り先頭から要素を取得し続けます。

条件を満たさなくなった時点で取得を停止します。

var takeWhileLessThan5 = numbers.TakeWhile(n => n < 5);
foreach (var num in takeWhileLessThan5)
{
    Console.WriteLine(num);
}
1
2
3
4

SkipWhileは、条件を満たす限り先頭から要素をスキップし、条件を満たさなくなった時点から残りの要素を取得します。

var skipWhileLessThan5 = numbers.SkipWhile(n => n < 5);
foreach (var num in skipWhileLessThan5)
{
    Console.WriteLine(num);
}
5
6
7
8
9
10

条件付き分割の応用

TakeWhileSkipWhileは、単純な数値比較だけでなく、複雑な条件やインデックスを利用した条件指定も可能です。

例えば、要素の値とインデックスの両方を使って条件を指定できます。

var takeWhileCondition = numbers.TakeWhile((n, index) => n > index);
foreach (var num in takeWhileCondition)
{
    Console.WriteLine(num);
}
1
2
3
4
5
6
7
8
9
10

この例では、要素の値がインデックスより大きい限り取得を続けています。

numbersは1から10までの連続した数値なので、すべての要素が条件を満たし、全要素が取得されます。

また、文字列のリストで特定のパターンが続く限り取得する例もあります。

var fruits = new List<string> { "apple", "apricot", "banana", "blueberry", "cherry" };
var takeWhileA = fruits.TakeWhile(fruit => fruit.StartsWith("a"));
foreach (var fruit in takeWhileA)
{
    Console.WriteLine(fruit);
}
apple
apricot

このように、TakeWhileは条件に合う連続した要素を取得し、途中で条件が外れると取得を停止します。

SkipWhileは逆に条件に合う連続した要素をスキップして残りを取得します。

これらのメソッドは、データの先頭や途中で条件に基づいて分割したい場合に非常に便利です。

セット演算子

Distinct

Distinctメソッドは、シーケンス内の重複する要素を除外し、一意な要素だけを返します。

デフォルトでは、要素の型がEqualsメソッドとGetHashCodeメソッドで比較可能である必要があります。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var numbers = new List<int> { 1, 2, 2, 3, 4, 4, 5 };
        // 重複を除外
        var distinctNumbers = numbers.Distinct();
        foreach (var num in distinctNumbers)
        {
            Console.WriteLine(num);
        }
    }
}
1
2
3
4
5

この例では、24の重複が除かれ、一意な値だけが取得されています。

Union

Unionメソッドは、2つのシーケンスの和集合を返します。

両方のシーケンスに含まれる要素は一度だけ含まれ、重複は除かれます。

var list1 = new List<int> { 1, 2, 3 };
var list2 = new List<int> { 3, 4, 5 };
var union = list1.Union(list2);
foreach (var num in union)
{
    Console.WriteLine(num);
}
1
2
3
4
5

Unionは、2つのシーケンスの要素を結合し、重複を排除した結果を返します。

Intersect

Intersectメソッドは、2つのシーケンスの共通部分(積集合)を返します。

両方のシーケンスに存在する要素だけが含まれます。

var list1 = new List<int> { 1, 2, 3, 4 };
var list2 = new List<int> { 3, 4, 5, 6 };
var intersect = list1.Intersect(list2);
foreach (var num in intersect)
{
    Console.WriteLine(num);
}
3
4

この例では、34が両方のリストに含まれているため結果に含まれています。

Except

Exceptメソッドは、最初のシーケンスから2番目のシーケンスに含まれる要素を除外した差集合を返します。

var list1 = new List<int> { 1, 2, 3, 4, 5 };
var list2 = new List<int> { 3, 4 };
var except = list1.Except(list2);
foreach (var num in except)
{
    Console.WriteLine(num);
}
1
2
5

この例では、list2に含まれる34list1から除外されています。

カスタム等価比較

DistinctUnionIntersectExceptは、デフォルトの等価比較EqualsGetHashCodeを使いますが、独自の比較ロジックを使いたい場合はIEqualityComparer<T>を実装したクラスを渡すことができます。

例えば、以下のようにカスタムの等価比較子を作成し、オブジェクトの特定のプロパティで比較することが可能です。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var people1 = new List<Person>
        {
            new Person { Name = "Alice", Age = 30 },
            new Person { Name = "Bob", Age = 25 }
        };
        var people2 = new List<Person>
        {
            new Person { Name = "alice", Age = 35 },
            new Person { Name = "Charlie", Age = 40 }
        };
        // 名前を大文字小文字を区別せず比較するカスタム比較子を使用してUnion
        var union = people1.Union(people2, new PersonNameComparer());
        foreach (var person in union)
        {
            Console.WriteLine($"{person.Name} ({person.Age})");
        }
    }
}
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
class PersonNameComparer : IEqualityComparer<Person>
{
    public bool Equals(Person x, Person y)
    {
        if (ReferenceEquals(x, y)) return true;
        if (x is null || y is null) return false;
        return string.Equals(x.Name, y.Name, StringComparison.OrdinalIgnoreCase);
    }
    public int GetHashCode(Person obj)
    {
        return obj.Name?.ToLowerInvariant().GetHashCode() ?? 0;
    }
}
Alice (30)
Bob (25)
Charlie (40)

この例では、PersonNameComparerを使って名前を大文字小文字を区別せずに比較しています。

people1Alicepeople2aliceは同じ名前とみなされ、重複が排除されています。

カスタム等価比較子を使うことで、複雑なオブジェクトの集合演算を柔軟に制御できます。

生成演算子

Range

Rangeメソッドは、指定した開始値から連続した整数のシーケンスを生成します。

開始値と生成する要素数を引数に取り、IEnumerable<int>を返します。

例えば、1から10までの整数を生成する場合は以下のように使います。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        // 1から10までの整数を生成
        var numbers = Enumerable.Range(1, 10);
        foreach (var num in numbers)
        {
            Console.WriteLine(num);
        }
    }
}
1
2
3
4
5
6
7
8
9
10

Rangeはループの代わりに連続した数値のシーケンスを簡単に作成したいときに便利です。

Repeat

Repeatメソッドは、指定した要素を指定回数繰り返すシーケンスを生成します。

例えば、文字列を5回繰り返す場合は以下のように使います。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        // "Hello"を5回繰り返すシーケンスを生成
        var repeated = Enumerable.Repeat("Hello", 5);
        foreach (var str in repeated)
        {
            Console.WriteLine(str);
        }
    }
}
Hello
Hello
Hello
Hello
Hello

Repeatは同じ値を複数回使いたい場合や、初期値のシーケンスを作成するときに役立ちます。

Empty

Emptyメソッドは、指定した型の空のシーケンスを返します。

要素が一つもないシーケンスを表現したいときに使います。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        // int型の空のシーケンスを生成
        var emptySequence = Enumerable.Empty<int>();
        Console.WriteLine($"要素数: {emptySequence.Count()}");
    }
}
要素数: 0

Emptyは、条件によっては空のシーケンスを返す必要がある場合や、初期化時に空のシーケンスを用意したい場合に便利です。

DefaultIfEmpty

DefaultIfEmptyメソッドは、シーケンスが空の場合に既定値を返すシーケンスに変換します。

空でない場合は元のシーケンスをそのまま返します。

引数に既定値を指定することも可能です。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var emptyList = new List<int>();
        // 空のシーケンスに対して既定値0を返す
        var result = emptyList.DefaultIfEmpty(0);
        foreach (var num in result)
        {
            Console.WriteLine(num);
        }
    }
}
0

空でないシーケンスの場合は元の要素がそのまま返ります。

var numbers = new List<int> { 1, 2, 3 };
var result2 = numbers.DefaultIfEmpty(0);
foreach (var num in result2)
{
    Console.WriteLine(num);
}
1
2
3

DefaultIfEmptyは、空のシーケンスに対して安全に既定値を返したい場合に使います。

例えば、集計結果が空の場合に0を返すなどの用途に便利です。

変換演算子

Cast

Castメソッドは、非ジェネリックなIEnumerableをジェネリックなIEnumerable<T>に変換します。

要素が指定した型にキャスト可能であることが前提です。

キャストできない要素があると例外が発生します。

using System;
using System.Linq;
using System.Collections;
class Program
{
    static void Main()
    {
        IEnumerable objects = new ArrayList { 1, 2, 3, 4 };
        // IEnumerableをIEnumerable<int>にキャスト
        var numbers = objects.Cast<int>();
        foreach (var num in numbers)
        {
            Console.WriteLine(num);
        }
    }
}
1
2
3
4

Castは、非ジェネリックコレクションをジェネリックに扱いたい場合に使いますが、型が合わないとInvalidCastExceptionが発生するため注意が必要です。

OfType

OfTypeメソッドは、シーケンス内の指定した型にキャスト可能な要素だけを抽出します。

キャストできない要素は無視されるため、Castより安全に使えます。

using System;
using System.Linq;
using System.Collections;
class Program
{
    static void Main()
    {
        IEnumerable mixed = new ArrayList { 1, "two", 3, "four", 5 };
        // int型の要素だけを抽出
        var numbers = mixed.OfType<int>();
        foreach (var num in numbers)
        {
            Console.WriteLine(num);
        }
    }
}
1
3
5

OfTypeは、異なる型が混在するコレクションから特定の型の要素だけを取り出したい場合に便利です。

ToList / ToArray

ToListメソッドは、シーケンスをList<T>に変換します。

ToArrayT[]の配列に変換します。

どちらも即時実行され、結果をメモリ上に確保します。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var numbers = Enumerable.Range(1, 5);
        // List<int>に変換
        List<int> list = numbers.ToList();
        // int[]に変換
        int[] array = numbers.ToArray();
        Console.WriteLine("Listの要素:");
        foreach (var num in list)
        {
            Console.WriteLine(num);
        }
        Console.WriteLine("配列の要素:");
        foreach (var num in array)
        {
            Console.WriteLine(num);
        }
    }
}
Listの要素:
1
2
3
4
5
配列の要素:
1
2
3
4
5

ToListToArrayは、遅延実行のシーケンスを即時に評価し、複数回の列挙を避けたい場合や、リストや配列のメソッドを使いたい場合に使います。

ToDictionary

ToDictionaryメソッドは、シーケンスの要素をキーと値のペアに変換し、Dictionary<TKey, TValue>を生成します。

キーセレクタと値セレクタを指定して、キーと値を自由に設定できます。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var people = new List<Person>
        {
            new Person { Id = 1, Name = "Alice" },
            new Person { Id = 2, Name = "Bob" },
            new Person { Id = 3, Name = "Charlie" }
        };
        // Idをキー、Nameを値にして辞書を作成
        var dictionary = people.ToDictionary(p => p.Id, p => p.Name);
        foreach (var kvp in dictionary)
        {
            Console.WriteLine($"Key: {kvp.Key}, Value: {kvp.Value}");
        }
    }
}
class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
}
Key: 1, Value: Alice
Key: 2, Value: Bob
Key: 3, Value: Charlie

キー重複対策

ToDictionaryはキーの重複を許さず、重複があるとArgumentExceptionが発生します。

重複を回避するには、事前に重複を除外するか、グループ化して処理する方法があります。

例えば、重複キーを持つ要素がある場合に最初の要素だけを辞書に登録する例です。

var peopleWithDuplicates = new List<Person>
{
    new Person { Id = 1, Name = "Alice" },
    new Person { Id = 2, Name = "Bob" },
    new Person { Id = 1, Name = "Alex" } // Idが重複
};
// 重複キーの最初の要素だけを辞書に登録
var dictionary = peopleWithDuplicates
    .GroupBy(p => p.Id)
    .ToDictionary(g => g.Key, g => g.First().Name);
foreach (var kvp in dictionary)
{
    Console.WriteLine($"Key: {kvp.Key}, Value: {kvp.Value}");
}
Key: 1, Value: Alice
Key: 2, Value: Bob

このように、GroupByでキーごとにグループ化し、各グループの最初の要素を辞書の値として使うことで重複を回避できます。

重複キーの扱いは要件に応じて適切に設計してください。

要素演算子

First / FirstOrDefault

Firstメソッドは、シーケンスの最初の要素を取得します。

条件を指定するオーバーロードもあり、条件に合致する最初の要素を返します。

要素が存在しない場合は例外InvalidOperationExceptionが発生します。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var numbers = new List<int> { 10, 20, 30, 40 };
        // 最初の要素を取得
        int first = numbers.First();
        Console.WriteLine($"最初の要素: {first}");
        // 25より大きい最初の要素を取得
        int firstOver25 = numbers.First(n => n > 25);
        Console.WriteLine($"25より大きい最初の要素: {firstOver25}");
        // 空のリストでFirstを呼ぶと例外になるためコメントアウト
        // var empty = new List<int>();
        // int error = empty.First(); // 例外発生
    }
}
最初の要素: 10
25より大きい最初の要素: 30

FirstOrDefaultは、要素が存在しない場合に既定値(値型なら0、参照型ならnull)を返します。

例外が発生しないため安全に使えます。

var empty = new List<int>();
int defaultValue = empty.FirstOrDefault();
Console.WriteLine($"空のリストのFirstOrDefault: {defaultValue}");
空のリストのFirstOrDefault: 0

Last / LastOrDefault

Lastメソッドは、シーケンスの最後の要素を取得します。

条件付きのオーバーロードもあり、条件に合致する最後の要素を返します。

要素がない場合は例外が発生します。

var numbers = new List<int> { 10, 20, 30, 40, 50 };
// 最後の要素を取得
int last = numbers.Last();
Console.WriteLine($"最後の要素: {last}");
// 25より大きい最後の要素を取得
int lastOver25 = numbers.Last(n => n > 25);
Console.WriteLine($"25より大きい最後の要素: {lastOver25}");
最後の要素: 50
25より大きい最後の要素: 50

LastOrDefaultは、要素が存在しない場合に既定値を返します。

var empty = new List<int>();
int defaultLast = empty.LastOrDefault();
Console.WriteLine($"空のリストのLastOrDefault: {defaultLast}");
空のリストのLastOrDefault: 0

Single / SingleOrDefault

Singleメソッドは、シーケンスに要素がちょうど1つだけ存在する場合にその要素を返します。

要素が0個または2個以上ある場合は例外が発生します。

条件付きのオーバーロードもあります。

var singleItem = new List<int> { 42 };
// 要素が1つだけのシーケンスから取得
int single = singleItem.Single();
Console.WriteLine($"Singleの結果: {single}");
// 要素が複数あると例外になるためコメントアウト
// var multiple = new List<int> { 1, 2 };
// int error = multiple.Single(); // 例外発生
Singleの結果: 42

SingleOrDefaultは、要素が0個の場合は既定値を返し、2個以上の場合は例外が発生します。

var empty = new List<int>();
int defaultSingle = empty.SingleOrDefault();
Console.WriteLine($"空のリストのSingleOrDefault: {defaultSingle}");
空のリストのSingleOrDefault: 0

ElementAt / ElementAtOrDefault

ElementAtメソッドは、指定したインデックスの要素を取得します。

インデックスが範囲外の場合は例外ArgumentOutOfRangeExceptionが発生します。

var numbers = new List<int> { 10, 20, 30, 40 };
int second = numbers.ElementAt(1); // インデックスは0始まり
Console.WriteLine($"インデックス1の要素: {second}");
// 範囲外アクセスは例外になるためコメントアウト
// int error = numbers.ElementAt(10); // 例外発生
インデックス1の要素: 20

ElementAtOrDefaultは、インデックスが範囲外の場合に既定値を返します。

int defaultElement = numbers.ElementAtOrDefault(10);
Console.WriteLine($"範囲外アクセスのElementAtOrDefault: {defaultElement}");
範囲外アクセスのElementAtOrDefault: 0

これらの要素演算子は、シーケンスから特定の位置や条件に合う単一の要素を取得したい場合に使います。

例外が発生する可能性があるため、状況に応じてOrDefault付きのメソッドを使うと安全です。

量的判定演算子

Any

Anyメソッドは、シーケンスに要素が1つでも存在するかどうかを判定します。

引数なしで呼び出すと、空でないかどうかをチェックします。

条件付きのオーバーロードもあり、指定した条件に合致する要素が1つでもあればtrueを返します。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var numbers = new List<int> { 1, 3, 5, 7 };
        // シーケンスに要素があるか
        bool hasAny = numbers.Any();
        Console.WriteLine($"要素が存在するか: {hasAny}");
        // 偶数の要素があるか
        bool hasEven = numbers.Any(n => n % 2 == 0);
        Console.WriteLine($"偶数の要素が存在するか: {hasEven}");
    }
}
要素が存在するか: True
偶数の要素が存在するか: False

Anyは、空のシーケンスかどうかや条件に合う要素の有無を効率的に判定したい場合に使います。

All

Allメソッドは、シーケンスのすべての要素が指定した条件を満たすかどうかを判定します。

条件を満たさない要素が1つでもあればfalseを返します。

空のシーケンスに対しては常にtrueを返します。

var numbers = new List<int> { 2, 4, 6, 8 };
// すべての要素が偶数か
bool allEven = numbers.All(n => n % 2 == 0);
Console.WriteLine($"すべて偶数か: {allEven}");
var mixed = new List<int> { 2, 3, 4 };
bool allEvenMixed = mixed.All(n => n % 2 == 0);
Console.WriteLine($"すべて偶数か(混合): {allEvenMixed}");
すべて偶数か: True
すべて偶数か(混合): False

Allは、全要素が条件を満たしているかをチェックしたい場合に使います。

Contains

Containsメソッドは、シーケンスに指定した要素が含まれているかどうかを判定します。

デフォルトの等価比較を使いますが、カスタムのIEqualityComparer<T>を指定することも可能です。

var fruits = new List<string> { "apple", "banana", "cherry" };
// "banana"が含まれているか
bool hasBanana = fruits.Contains("banana");
Console.WriteLine($"bananaが含まれているか: {hasBanana}");
// "grape"が含まれているか
bool hasGrape = fruits.Contains("grape");
Console.WriteLine($"grapeが含まれているか: {hasGrape}");
bananaが含まれているか: True
grapeが含まれているか: False

Containsは、特定の値がシーケンスに存在するかを簡単に調べるのに便利です。

SequenceEqual

SequenceEqualメソッドは、2つのシーケンスが要素の順序も含めて完全に等しいかどうかを判定します。

要素の比較はデフォルトの等価比較を使いますが、カスタムのIEqualityComparer<T>を指定することも可能です。

var list1 = new List<int> { 1, 2, 3 };
var list2 = new List<int> { 1, 2, 3 };
var list3 = new List<int> { 3, 2, 1 };
bool equal1 = list1.SequenceEqual(list2);
bool equal2 = list1.SequenceEqual(list3);
Console.WriteLine($"list1とlist2は等しいか: {equal1}");
Console.WriteLine($"list1とlist3は等しいか: {equal2}");
list1とlist2は等しいか: True
list1とlist3は等しいか: False

SequenceEqualは、シーケンスの内容と順序が完全に一致しているかを確認したい場合に使います。

例えば、テストの検証やデータの比較に役立ちます。

異常処理とクエリの安全性

例外の発生ポイント

LINQのクエリは遅延実行が基本であるため、例外が発生するタイミングが直感的でない場合があります。

クエリの定義時には例外は発生せず、実際に列挙や即時実行メソッド(ToList(), Count(), First()など)を呼び出した際に例外が発生します。

例えば、First()メソッドは条件に合致する要素が存在しない場合にInvalidOperationExceptionをスローしますが、これはクエリ定義時ではなく、列挙時に発生します。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var numbers = new List<int> { 1, 2, 3 };
        var query = numbers.Where(n => n > 5);
        try
        {
            // ここで初めてクエリが実行され、例外が発生する可能性がある
            int first = query.First();
            Console.WriteLine(first);
        }
        catch (InvalidOperationException ex)
        {
            Console.WriteLine("例外発生: " + ex.Message);
        }
    }
}
例外発生: Sequence contains no elements

また、ElementAtで範囲外のインデックスを指定した場合や、ToDictionaryでキーの重複がある場合も例外が発生します。

これらの例外は、クエリの実行時に発生するため、例外処理はクエリの呼び出し側で適切に行う必要があります。

Null安全なクエリ構築

LINQクエリを安全に扱うためには、null参照に対する対策が重要です。

特に、シーケンス自体や要素のプロパティがnullの場合に例外が発生しやすいため、nullチェックを適切に行うことが求められます。

例えば、以下のようにnullの可能性があるシーケンスに対してWhereSelectを使うとNullReferenceExceptionが発生します。

List<string> names = null;
// ここでNullReferenceExceptionが発生する
var query = names.Where(name => name.StartsWith("A"));

このような場合は、nullチェックを行い、nullの場合は空のシーケンスを返すようにすると安全です。

var safeNames = names ?? Enumerable.Empty<string>();
var query = safeNames.Where(name => name.StartsWith("A"));
foreach (var name in query)
{
    Console.WriteLine(name);
}

また、要素のプロパティがnullの可能性がある場合は、条件式内でnullチェックを行います。

var people = new List<Person>
{
    new Person { Name = "Alice" },
    new Person { Name = null },
    new Person { Name = "Bob" }
};
var filtered = people.Where(p => p.Name != null && p.Name.StartsWith("A"));
foreach (var person in filtered)
{
    Console.WriteLine(person.Name);
}
Alice

C# 8.0以降では、nullable参照型機能を活用して、コンパイル時にnullの可能性を検出しやすくすることもできます。

さらに、SelectSelectManynullを返す可能性がある場合は、Wherenullを除外するか、DefaultIfEmptyを使って既定値を設定する方法もあります。

var results = people
    .Select(p => p.Name)
    .Where(name => name != null);

このように、LINQクエリをnull安全に構築することで、実行時の例外を防ぎ、堅牢なコードを書くことができます。

パフォーマンス最適化

Materializeのタイミング

LINQのクエリは基本的に遅延実行されます。

つまり、クエリの定義時には実際のデータ処理は行われず、結果が必要になった時点で初めて処理が実行されます。

この遅延実行の特性は効率的ですが、クエリの結果を複数回利用する場合には注意が必要です。

クエリの結果を一度に評価してメモリ上に確保することを「Materialize(マテリアライズ)」と呼びます。

ToList()ToArray()などのメソッドを使うと即時実行され、結果がマテリアライズされます。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var numbers = Enumerable.Range(1, 5);
        // 遅延実行のまま複数回列挙すると毎回処理が実行される
        Console.WriteLine("遅延実行のまま複数回列挙:");
        foreach (var n in numbers.Where(x => x % 2 == 0))
            Console.WriteLine(n);
        foreach (var n in numbers.Where(x => x % 2 == 0))
            Console.WriteLine(n);
        // ToListでマテリアライズしておくと処理は一度だけ
        var materialized = numbers.Where(x => x % 2 == 0).ToList();
        Console.WriteLine("マテリアライズ後の複数回列挙:");
        foreach (var n in materialized)
            Console.WriteLine(n);
        foreach (var n in materialized)
            Console.WriteLine(n);
    }
}
遅延実行のまま複数回列挙:
2
4
2
4
マテリアライズ後の複数回列挙:
2
4
2
4

上記の例では、遅延実行のままだとWhereの条件判定が毎回実行されますが、ToList()でマテリアライズすると一度だけ評価され、以降はメモリ上のリストを参照します。

大量データや重い処理の場合はマテリアライズのタイミングを意識することが重要です。

不要な再列挙の回避

LINQの遅延実行により、同じクエリを複数回列挙すると、その都度処理が繰り返されます。

これがパフォーマンス低下の原因になることがあります。

特に、データソースがデータベースやファイルなどの外部リソースの場合は注意が必要です。

var query = dataSource.Where(x => x.IsActive);
// 複数回列挙すると毎回Whereが実行される
foreach (var item in query)
    Process(item);
foreach (var item in query)
    ProcessAgain(item);

このような場合は、ToList()ToArray()で一度マテリアライズしてから複数回利用することで、不要な再列挙を防げます。

var cached = dataSource.Where(x => x.IsActive).ToList();
foreach (var item in cached)
    Process(item);
foreach (var item in cached)
    ProcessAgain(item);

また、IEnumerable<T>を返すメソッドを設計する際も、呼び出し側での再列挙を考慮し、必要に応じてマテリアライズを促すドキュメントを残すと良いでしょう。

バッファリング戦略

LINQの一部の演算子は内部でバッファリング(中間結果の一時保存)を行います。

例えば、OrderByGroupByは全要素を一旦読み込んでから処理を行うため、メモリ使用量が増加します。

バッファリングは処理の効率化や遅延実行の実現に役立ちますが、大量データを扱う場合はメモリ消費に注意が必要です。

var largeData = Enumerable.Range(1, 1000000);
// OrderByは全要素をバッファリングしてから並べ替え
var ordered = largeData.OrderBy(x => -x);
// 遅延実行だが、列挙時に大量のメモリを使う可能性がある
foreach (var n in ordered.Take(10))
    Console.WriteLine(n);

バッファリングを避けたい場合は、WhereSelectなどのストリーム処理を優先し、必要な部分だけを処理するように設計します。

また、TakeSkipなどの分割演算子を組み合わせて処理範囲を限定することも効果的です。

さらに、IQueryable<T>を使ったデータベースクエリでは、プロバイダーがSQLに変換して効率的に処理するため、LINQのバッファリングの影響は異なります。

実行環境に応じて適切な戦略を選択してください。

パフォーマンス最適化では、遅延実行の特性を理解し、マテリアライズのタイミングや再列挙の回避、バッファリングの影響を考慮した設計が重要です。

これにより、効率的でスケーラブルなLINQクエリを実現できます。

実践シナリオ別サンプル

フィルタリングと投影の組み合わせ

フィルタリングと投影を組み合わせることで、必要なデータだけを抽出しつつ、形を変えて扱いやすくすることができます。

例えば、社員リストから30歳以上の社員の名前と年齢だけを抽出するケースを考えます。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var employees = new List<Employee>
        {
            new Employee { Name = "Alice", Age = 28, Department = "Sales" },
            new Employee { Name = "Bob", Age = 35, Department = "HR" },
            new Employee { Name = "Charlie", Age = 32, Department = "IT" },
            new Employee { Name = "David", Age = 25, Department = "Sales" }
        };
        // 30歳以上の社員の名前と年齢を抽出
        var filteredProjected = employees
            .Where(e => e.Age >= 30)
            .Select(e => new { e.Name, e.Age });
        foreach (var item in filteredProjected)
        {
            Console.WriteLine($"Name: {item.Name}, Age: {item.Age}");
        }
    }
}
class Employee
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Department { get; set; }
}
Name: Bob, Age: 35
Name: Charlie, Age: 32

この例では、Whereで30歳以上の社員をフィルタリングし、Selectで匿名型に投影しています。

必要な情報だけを抽出し、扱いやすい形に変換する典型的なパターンです。

集計結果を辞書へ変換

集計した結果を辞書に変換すると、キーで高速にアクセスできるようになります。

例えば、部署ごとの社員数を集計し、部署名をキー、社員数を値とする辞書を作成する例です。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var employees = new List<Employee>
        {
            new Employee { Name = "Alice", Department = "Sales" },
            new Employee { Name = "Bob", Department = "HR" },
            new Employee { Name = "Charlie", Department = "Sales" },
            new Employee { Name = "David", Department = "IT" },
            new Employee { Name = "Eve", Department = "HR" }
        };
        // 部署ごとに社員数を集計し、辞書に変換
        var departmentCounts = employees
            .GroupBy(e => e.Department)
            .ToDictionary(g => g.Key, g => g.Count());
        foreach (var kvp in departmentCounts)
        {
            Console.WriteLine($"Department: {kvp.Key}, Count: {kvp.Value}");
        }
    }
}
class Employee
{
    public string Name { get; set; }
    public string Department { get; set; }
}
Department: Sales, Count: 2
Department: HR, Count: 2
Department: IT, Count: 1

GroupByで部署ごとにグループ化し、ToDictionaryでキーを部署名、値をグループの要素数にしています。

これにより、部署ごとの社員数を効率的に管理できます。

複数シーケンスのマージ

複数のシーケンスを結合して一つのシーケンスにまとめることもよくあります。

例えば、2つのリストの社員情報をマージし、重複を除外して一つのリストにする例です。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var list1 = new List<Employee>
        {
            new Employee { Id = 1, Name = "Alice" },
            new Employee { Id = 2, Name = "Bob" }
        };
        var list2 = new List<Employee>
        {
            new Employee { Id = 2, Name = "Bob" },
            new Employee { Id = 3, Name = "Charlie" }
        };
        // Idで重複を除外しながらマージ
        var merged = list1
            .Union(list2, new EmployeeIdComparer())
            .ToList();
        foreach (var emp in merged)
        {
            Console.WriteLine($"Id: {emp.Id}, Name: {emp.Name}");
        }
    }
}
class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
}
class EmployeeIdComparer : IEqualityComparer<Employee>
{
    public bool Equals(Employee x, Employee y)
    {
        if (ReferenceEquals(x, y)) return true;
        if (x is null || y is null) return false;
        return x.Id == y.Id;
    }
    public int GetHashCode(Employee obj)
    {
        return obj.Id.GetHashCode();
    }
}
Id: 1, Name: Alice
Id: 2, Name: Bob
Id: 3, Name: Charlie

この例では、Unionにカスタムの等価比較子を渡して、Idが同じ社員を重複とみなして除外しています。

複数のシーケンスを効率的にマージし、重複を防ぐ典型的な方法です。

カスタム拡張メソッドの作成

基本的な実装手順

カスタム拡張メソッドは、既存の型に新しいメソッドを追加するための便利な手段です。

LINQの標準クエリ演算子のように、自分で独自の操作を定義して使いたい場合に活用します。

拡張メソッドは静的クラスの静的メソッドとして実装し、第一引数にthisキーワードを付けて対象の型を指定します。

以下は、IEnumerable<T>に対して要素の合計を計算する簡単なカスタム拡張メソッドの例です。

using System;
using System.Collections.Generic;
static class EnumerableExtensions
{
    // IEnumerable<int>の合計を計算する拡張メソッド
    public static int SumCustom(this IEnumerable<int> source)
    {
        if (source == null) throw new ArgumentNullException(nameof(source));
        int sum = 0;
        foreach (var item in source)
        {
            sum += item;
        }
        return sum;
    }
}
class Program
{
    static void Main()
    {
        var numbers = new List<int> { 1, 2, 3, 4, 5 };
        // カスタム拡張メソッドを呼び出す
        int total = numbers.SumCustom();
        Console.WriteLine($"合計: {total}");
    }
}
合計: 15

この例では、SumCustomメソッドをIEnumerable<int>の拡張メソッドとして定義しています。

this IEnumerable<int> sourceの部分が拡張メソッドのポイントで、呼び出し側はまるでList<int>のインスタンスメソッドのように使えます。

実装のポイントは以下の通りです。

  • 静的クラス内に静的メソッドとして定義します
  • 第一引数にthisを付けて拡張対象の型を指定します
  • 引数のnullチェックを行い、例外を投げるなど安全性を確保します
  • 必要に応じてジェネリック型を使い、汎用性を持たせます

ジェネリック制約の活用

カスタム拡張メソッドをジェネリックに実装することで、様々な型に対応可能な柔軟なメソッドを作成できます。

ジェネリック制約を使うと、型パラメータに対して特定の条件を課し、メソッド内で安全に操作できるようになります。

例えば、IEnumerable<T>の中から条件に合う最初の要素を返す拡張メソッドを作成します。

ここでは、TIComparable<T>を実装していることを制約として指定し、比較可能な型に限定します。

using System;
using System.Collections.Generic;
static class EnumerableExtensions
{
    // 条件に合う最初の要素を返す(IComparable<T>制約付き)
    public static T FirstMatching<T>(this IEnumerable<T> source, Func<T, bool> predicate) where T : IComparable<T>
    {
        if (source == null) throw new ArgumentNullException(nameof(source));
        if (predicate == null) throw new ArgumentNullException(nameof(predicate));
        foreach (var item in source)
        {
            if (predicate(item))
            {
                return item;
            }
        }
        throw new InvalidOperationException("条件に合う要素が見つかりません。");
    }
}
class Program
{
    static void Main()
    {
        var numbers = new List<int> { 10, 20, 30, 40 };
        // 25より大きい最初の要素を取得
        int result = numbers.FirstMatching(n => n > 25);
        Console.WriteLine($"条件に合う最初の要素: {result}");
    }
}
条件に合う最初の要素: 30

この例では、where T : IComparable<T>という制約を付けることで、Tが比較可能な型であることを保証しています。

これにより、メソッド内で比較操作を安全に行うことができます。

ジェネリック制約には以下のような種類があります。

  • where T : class — 参照型に限定
  • where T : struct — 値型に限定
  • where T : new() — 引数なしコンストラクタを持つ型に限定
  • where T : 基底クラス名 — 特定のクラスを継承した型に限定
  • where T : インターフェース名 — 特定のインターフェースを実装した型に限定

これらを組み合わせて使うことも可能です。

カスタム拡張メソッドをジェネリックかつ制約付きで実装することで、より安全で汎用的なLINQ風のメソッドを作成できます。

開発の際は、対象とする型の特性に応じて適切な制約を設定しましょう。

非同期LINQの活用

AsAsyncEnumerable

AsAsyncEnumerableは、同期的なIEnumerable<T>を非同期のIAsyncEnumerable<T>に変換する拡張メソッドです。

これにより、既存の同期コレクションを非同期ストリームとして扱い、await foreach構文で非同期に列挙できるようになります。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
class Program
{
    static async Task Main()
    {
        var numbers = Enumerable.Range(1, 5);
        // 同期コレクションを非同期ストリームに変換
        var asyncNumbers = numbers.AsAsyncEnumerable();
        await foreach (var num in asyncNumbers)
        {
            Console.WriteLine(num);
            await Task.Delay(100); // 非同期処理の例
        }
    }
}
1
2
3
4
5

この例では、AsAsyncEnumerableで同期的なIEnumerable<int>を非同期ストリームに変換し、await foreachで非同期に要素を取得しています。

Task.Delayを使って非同期処理のシミュレーションも行っています。

AsAsyncEnumerableは、既存の同期データを非同期処理の流れに組み込みたい場合に便利です。

ただし、元のデータは同期的に取得されるため、I/O待ちなどの非同期処理は含まれません。

非同期ストリームとの統合

C# 8.0以降で導入された非同期ストリームIAsyncEnumerable<T>は、非同期にデータを逐次取得するためのインターフェースです。

LINQの非同期版としてSystem.Linq.Asyncパッケージが提供されており、IAsyncEnumerable<T>に対してもWhereSelectなどのLINQ演算子を非同期に適用できます。

以下は、非同期ストリームから条件に合う要素を非同期にフィルタリングし、変換する例です。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;

class Program
{
    static async Task Main()
    {
        // 非同期ストリームを取得
        var asyncNumbers = GetNumbersAsync();

        // 非同期ストリームを逐次処理しながら、偶数を2倍して出力
        await foreach (var number in asyncNumbers)
        {
            if (number % 2 == 0)
            {
                int doubled = number * 2; // 2倍に変換
                Console.WriteLine(doubled);
            }
        }
    }

    // 1~5 の数値を1つずつ遅延して返す非同期ストリーム
    static async IAsyncEnumerable<int> GetNumbersAsync()
    {
        for (int i = 1; i <= 5; i++)
        {
            await Task.Delay(100); // 非同期処理をシミュレーション
            yield return i;
        }
    }
}
4
8

この例では、GetNumbersAsyncが非同期ストリームを返し、WhereSelectの非同期版を使ってフィルタリングと変換を行っています。

await foreachで非同期に結果を列挙しています。

非同期LINQを使うには、System.Linq.AsyncパッケージをNuGetからインストールし、using System.Linq;のほかにusing System.Linq.Async;を追加します。

これにより、IAsyncEnumerable<T>に対してもLINQメソッド構文が利用可能になります。

非同期ストリームとLINQの統合により、I/O待ちやネットワーク通信などの非同期データソースを効率的に処理でき、UIの応答性向上やスケーラブルなサーバーアプリケーションの構築に役立ちます。

Queryable対応の注意点

IQueryable<T>と遅延実行

IQueryable<T>は、LINQのクエリをデータベースやリモートサービスなどの外部データソースに対して実行するためのインターフェースです。

IQueryable<T>IEnumerable<T>を継承していますが、クエリの実行方法や遅延実行の挙動に特徴があります。

IQueryable<T>のクエリは、式ツリー(Expression Tree)として構築され、実際のデータ取得はクエリが評価されるまで遅延されます。

つまり、クエリの定義時にはデータベースに問い合わせは発生せず、ToList(), First(), Count()などの即時実行メソッドが呼ばれたタイミングでSQLなどに変換されて実行されます。

using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
class Program
{
    static void Main()
    {
        using var context = new SampleDbContext();
        // IQueryableのクエリ定義(まだ実行されていない)
        IQueryable<Employee> query = context.Employees.Where(e => e.Age > 30);
        // ここで初めてSQLが発行される(遅延実行)
        var list = query.ToList();
        foreach (var emp in list)
        {
            Console.WriteLine($"{emp.Name} ({emp.Age})");
        }
    }
}

この遅延実行の特性により、クエリを組み合わせて複雑な条件を動的に構築できる一方で、クエリの実行タイミングや内容を意識しないとパフォーマンス問題や予期しない動作が起こることがあります。

データベースプロバイダーの変換制約

IQueryable<T>のクエリは、LINQ式ツリーをデータベースのクエリ言語(主にSQL)に変換して実行されます。

この変換はデータベースプロバイダー(例:Entity Framework Core、NHibernateなど)が担当しますが、すべての.NETのメソッドや演算子がSQLに変換できるわけではありません。

例えば、以下のようなメソッドはSQLに変換できず、例外が発生したり、クライアント側で処理されてパフォーマンスが低下したりします。

  • カスタムメソッドや.NET固有のメソッド
  • 一部の文字列操作や日付操作
  • 複雑なラムダ式や匿名型の操作
var query = context.Employees
    .Where(e => CustomCheck(e.Name)); // CustomCheckはSQLに変換不可
var list = query.ToList(); // 実行時に例外が発生する可能性がある
bool CustomCheck(string name)
{
    return name.StartsWith("A");
}

このような場合、CustomCheckはSQLに変換できないため、例外が発生するか、全件取得後にメモリ上でフィルタリングされてしまいます。

対策としては以下の方法があります。

  • クエリ内で使用するメソッドは、データベースプロバイダーがサポートする標準的なLINQメソッドや式に限定します
  • カスタムメソッドはクエリ外で実行し、結果を取得してからメモリ上で処理します
  • 必要に応じてAsEnumerable()ToList()でクエリを強制的に即時実行し、以降の処理をメモリ上で行います
var query = context.Employees
    .Where(e => e.Name.StartsWith("A")) // SQLに変換可能な式
    .AsEnumerable()                      // ここでSQL実行
    .Where(e => CustomCheck(e.Name));   // メモリ上でのフィルタリング
var list = query.ToList();

このように、IQueryable<T>を使う際は、データベースプロバイダーの変換制約を理解し、SQLに変換可能な式を使うことが重要です。

変換できない処理はクエリの外に出すか、明示的にメモリ上で処理することでパフォーマンスと安全性を確保できます。

よくある落とし穴と対策

ToList忘れによる複数列挙

LINQのクエリは遅延実行されるため、同じクエリを複数回列挙すると、その都度クエリが再評価されます。

これにより、パフォーマンスの低下や予期しない副作用が発生することがあります。

特に、データベースや外部リソースに対するクエリの場合は、複数回の問い合わせが発生してしまいます。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var numbers = Enumerable.Range(1, 5);
        var query = numbers.Where(n => n % 2 == 0);
        // 1回目の列挙
        foreach (var num in query)
        {
            Console.WriteLine(num);
        }
        // 2回目の列挙(再度Whereが評価される)
        foreach (var num in query)
        {
            Console.WriteLine(num);
        }
    }
}
2
4
2
4

このように、queryを2回列挙するとWhereの条件が2回評価されます。

大量データや重い処理の場合は無駄なコストになります。

対策としては、ToList()ToArray()を使って一度結果をメモリ上に確保し、複数回の列挙を防ぐことが推奨されます。

var cached = numbers.Where(n => n % 2 == 0).ToList();
foreach (var num in cached)
{
    Console.WriteLine(num);
}
foreach (var num in cached)
{
    Console.WriteLine(num);
}
2
4
2
4

この場合、Whereの評価はToList()の時点で一度だけ行われ、以降はメモリ上のリストを参照するため効率的です。

破壊的変更と状態不整合

LINQのクエリは元のデータソースを変更しませんが、元のコレクションがクエリの実行前後で変更されると、予期しない結果や状態不整合が発生することがあります。

特に、遅延実行のクエリを定義した後に元のコレクションを変更すると、列挙時に変更が反映されるため注意が必要です。

var list = new List<int> { 1, 2, 3 };
var query = list.Where(n => n > 1);
// 元のリストに要素を追加
list.Add(4);
// クエリを列挙すると変更後のデータが反映される
foreach (var num in query)
{
    Console.WriteLine(num);
}
2
3
4

この例では、queryを定義した時点ではlist4はありませんでしたが、列挙時に4が追加されているため結果に含まれています。

これが意図しない動作になることがあります。

また、元のコレクションがスレッドセーフでない場合、並行して変更が行われると例外や不整合が発生するリスクもあります。

対策としては以下の方法があります。

  • クエリを定義したらすぐにToList()ToArray()でマテリアライズし、元のコレクションの変更の影響を受けないようにします
  • 元のコレクションを変更する場合は、クエリの実行が完了していることを確認します
  • スレッドセーフなコレクションを使うか、適切な同期処理を行います
var snapshot = list.Where(n => n > 1).ToList();
// 元のリストを変更してもsnapshotには影響しない
list.Add(5);
foreach (var num in snapshot)
{
    Console.WriteLine(num);
}
2
3
4

このように、マテリアライズしておくことで状態の不整合を防ぎ、安定した結果を得られます。

LINQを使う際は、元データの変更タイミングとクエリの実行タイミングを意識して設計することが重要です。

まとめ

本記事では、C#のLINQメソッド構文の基本から応用まで幅広く解説しました。

拡張メソッドやラムダ式を活用した柔軟なデータ操作、フィルタリングや投影、並べ替え、集計、グループ化、結合などの主要な演算子の使い方を具体例とともに紹介しています。

また、遅延実行の特性やパフォーマンス最適化、非同期LINQの活用、IQueryable<T>の注意点、よくある落とし穴とその対策も理解できます。

これらを踏まえれば、効率的で安全なLINQクエリの作成が可能となり、実務でのデータ処理に役立てられます。

関連記事

Back to top button
目次へ