LINQ

【C#】LINQでわかるデータ操作テクニック総まとめ

LINQを使うと、コレクションやDB、XMLなど異なるデータ源へも同一のクエリ構文でアクセスでき、WhereSelectなどの拡張メソッドを連鎖させてフィルタ・変換・集計が簡潔になります。

遅延評価により不要な処理を避けつつ、ToList等で即時実行も選べ、ラムダ式と組み合わせて可読性と保守性が向上します。

ただし大規模データではメモリ消費と実行タイミングに注意が必要です。

目次から探す
  1. LINQの基礎知識
  2. 主要データソース別LINQ
  3. 基本操作
  4. コントロールフロー系演算子
  5. 生成演算子
  6. 変換演算子
  7. パフォーマンス最適化
  8. エラーと例外処理
  9. カスタム拡張メソッド
  10. 非同期LINQパターン
  11. 実務での活用例
  12. よくある落とし穴
  13. バージョン別の注意点
  14. 便利な補助API
  15. LINQと設計パターン
  16. まとめ

LINQの基礎知識

LINQ(Language Integrated Query)は、C#に組み込まれたデータ操作のための強力な機能です。

配列やリスト、データベース、XMLなど、さまざまなデータソースに対して統一的な方法でクエリを記述できるため、コードの可読性や保守性が大幅に向上します。

ここではLINQの基礎となる概念を丁寧に解説いたします。

.NET標準クエリ演算子とは

.NET標準クエリ演算子は、LINQの中核をなすメソッド群で、System.Linq名前空間に定義されています。

これらの演算子は、IEnumerable<T>やIQueryable<T>に対して適用でき、データのフィルタリング、並べ替え、投影、結合、集計など多彩な操作を可能にします。

代表的な標準クエリ演算子には以下のようなものがあります。

演算子名機能説明
Where条件に合致する要素を抽出する
Select要素を別の形に変換する
OrderBy昇順で並べ替える
OrderByDescending降順で並べ替える
GroupByキーに基づいて要素をグループ化する
Join2つのシーケンスを結合する
Aggregateカスタム集計を行う
Count要素の数を数える
Sum要素の合計を計算する
Any条件に合致する要素が存在するか判定する
First条件に合致する最初の要素を取得する

これらの演算子はメソッドチェーンとして連結でき、複雑なデータ操作も簡潔に記述できます。

例えば、リストから偶数だけを抽出し、昇順に並べ替えて2倍に変換する場合は以下のように書けます。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = new List<int> { 1, 3, 8, 27, 4, 6 };
        var query = numbers
            .Where(x => x % 2 == 0)    // 偶数を抽出
            .OrderBy(x => x)           // 昇順に並べ替え
            .Select(x => x * 2);       // 2倍に変換
        foreach (var num in query)
        {
            Console.WriteLine(num);
        }
    }
}
8
12
16

このように.NET標準クエリ演算子は、データ操作の基本的なビルディングブロックとして機能します。

IEnumerableとIQueryableの違い

LINQを使う際に重要なインターフェースとしてIEnumerable<T>IQueryable<T>があります。

どちらもシーケンス(コレクション)を表しますが、用途や動作に違いがあります。

特徴IEnumerable<T>IQueryable<T>
名前空間System.Collections.GenericSystem.Linq
実行場所クライアント側(メモリ上)データソース側(例:データベース)
遅延実行はいはい
クエリの翻訳なし(LINQ to Objects)クエリ式を式ツリーに変換し翻訳可能
主な用途メモリ内コレクションの操作リモートデータソースへのクエリ発行
パフォーマンス小規模データに適している大規模データやDBアクセスに適している

IEnumerable<T>は、メモリ上のコレクションに対してLINQを適用する際に使います。

例えば、配列やList<T>などが該当します。

LINQのクエリはC#のコードとして実行され、すべての要素を列挙しながら処理します。

一方、IQueryable<T>は、LINQ to SQLやEntity Frameworkのように、データベースなどの外部データソースに対してクエリを発行する際に使います。

クエリは式ツリーとして表現され、SQLなどのネイティブクエリに変換されてデータベース側で実行されます。

これにより、必要なデータだけを効率的に取得できます。

以下は簡単な例です。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        // IEnumerableの例(メモリ内コレクション)
        IEnumerable<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
        var evenNumbers = numbers.Where(n => n % 2 == 0);
        Console.WriteLine("IEnumerableで偶数を抽出:");
        foreach (var n in evenNumbers)
        {
            Console.WriteLine(n);
        }
        // IQueryableの例(通常はORMなどで使用)
        // ここでは簡単のためIQueryableに変換してみるだけ
        IQueryable<int> queryableNumbers = numbers.AsQueryable();
        var queryableEven = queryableNumbers.Where(n => n % 2 == 0);
        Console.WriteLine("IQueryableで偶数を抽出:");
        foreach (var n in queryableEven)
        {
            Console.WriteLine(n);
        }
    }
}
IEnumerableで偶数を抽出:
2
4
IQueryableで偶数を抽出:
2
4

実際のデータベースアクセスでは、IQueryableのクエリはSQLに変換されてデータベース側で実行されるため、パフォーマンス面で大きなメリットがあります。

クエリ構文とメソッド構文の比較

LINQには2つの記述スタイルがあります。

1つはSQLに似た「クエリ構文」、もう1つはメソッドチェーンを使う「メソッド構文」です。

どちらも同じ処理を実現できますが、使い分けることでコードの可読性や柔軟性が変わります。

クエリ構文

クエリ構文は、fromwhereselectなどのキーワードを使い、SQLのような見た目でクエリを記述します。

初心者にもわかりやすく、複雑な結合やグループ化も直感的に書けます。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = new List<int> { 1, 3, 8, 27, 4, 6 };
        var query = from x in numbers
                    where x % 2 == 0
                    orderby x
                    select x * 2;
        foreach (var num in query)
        {
            Console.WriteLine(num);
        }
    }
}
8
12
16

メソッド構文

メソッド構文は、WhereOrderBySelectなどの拡張メソッドを連結して書きます。

ラムダ式を使うため柔軟で、メソッドチェーンの途中で条件分岐やカスタム処理を挟みやすい特徴があります。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = new List<int> { 1, 3, 8, 27, 4, 6 };
        var query = numbers
            .Where(x => x % 2 == 0)
            .OrderBy(x => x)
            .Select(x => x * 2);
        foreach (var num in query)
        {
            Console.WriteLine(num);
        }
    }
}
8
12
16

使い分けのポイント

  • クエリ構文はSQLに慣れている方や、複雑な結合・グループ化をわかりやすく書きたい場合に適しています
  • メソッド構文はラムダ式の柔軟性を活かしたい場合や、動的にクエリを組み立てる場合に便利です

実際には両者を混在させることも可能で、クエリ構文の中でメソッド構文を使うこともできます。

デリゲートとラムダ式の役割

LINQのメソッド構文では、条件や変換処理を指定するためにデリゲートやラムダ式を多用します。

これらは関数を変数のように扱う仕組みで、LINQの柔軟性を支える重要な要素です。

デリゲートとは

デリゲートは、メソッドの参照を保持できる型で、メソッドを引数として渡したり、変数に代入したりできます。

LINQの標準クエリ演算子は、条件や変換のロジックをデリゲートとして受け取ります。

例えば、WhereメソッドはFunc<T, bool>型のデリゲートを受け取り、条件に合う要素だけを抽出します。

Func<int, bool> isEven = delegate(int x) { return x % 2 == 0; };
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var evenNumbers = numbers.Where(isEven);
foreach (var n in evenNumbers)
{
    Console.WriteLine(n);
}
2
4

ラムダ式とは

ラムダ式は匿名関数の一種で、より簡潔にデリゲートを記述できます。

上記の例は以下のように書き換えられます。

var numbers = new List<int> { 1, 2, 3, 4, 5 };
var evenNumbers = numbers.Where(x => x % 2 == 0);
foreach (var n in evenNumbers)
{
    Console.WriteLine(n);
}

ラムダ式は引数リストの後に=>を置き、その右側に処理内容を記述します。

引数が1つの場合は括弧を省略でき、処理が単一式なら波括弧も省略可能です。

デリゲートとラムダ式の関係

  • デリゲートは型として存在し、メソッドの参照を保持します
  • ラムダ式は匿名関数の記法で、デリゲート型に暗黙的に変換されます

LINQのメソッドは、Func<T, TResult>Func<T, bool>などのデリゲートを引数に取るため、ラムダ式を使うことで簡潔に条件や変換処理を指定できます。

デリゲートとラムダ式はLINQの柔軟なクエリ記述を支える基盤です。

ラムダ式を使いこなすことで、複雑な条件や変換もシンプルに表現でき、コードの可読性が向上します。

LINQを使う際は、これらの仕組みを理解しておくことが重要です。

主要データソース別LINQ

LINQは多様なデータソースに対して統一的にクエリを記述できる点が大きな魅力です。

ここでは代表的なデータソースごとにLINQの使い方やポイントを具体的に解説いたします。

配列とList

配列やList<T>はLINQの基本的な対象であり、IEnumerable<T>を実装しているため、LINQの標準クエリ演算子をそのまま使えます。

例えば、整数の配列から偶数だけを抽出し、昇順に並べ替える例です。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 5, 2, 9, 1, 6, 4 };
        var evenSorted = numbers
            .Where(n => n % 2 == 0)
            .OrderBy(n => n);
        foreach (var num in evenSorted)
        {
            Console.WriteLine(num);
        }
    }
}
2
4
6

List<T>でも同様に使えます。

LINQは遅延実行のため、クエリを定義した時点では処理は行われず、列挙時に実行されます。

DictionaryとLookup

Dictionary<TKey, TValue>はキーと値のペアを保持するコレクションで、LINQでの操作も可能ですが、Dictionary自体はIEnumerable<KeyValuePair<TKey, TValue>>を実装しているため、キーや値にアクセスする際はKeyValueプロパティを使います。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var dict = new Dictionary<string, int>
        {
            ["apple"] = 3,
            ["banana"] = 5,
            ["orange"] = 2
        };
        // 値が3以上の要素を抽出
        var filtered = dict.Where(kv => kv.Value >= 3);
        foreach (var kv in filtered)
        {
            Console.WriteLine($"{kv.Key}: {kv.Value}");
        }
    }
}
apple: 3
banana: 5

一方、Lookup<TKey, TElement>はキーに対して複数の要素をグループ化したコレクションで、GroupByの結果として得られます。

Lookupは読み取り専用で、キーごとに複数の値を効率的に取得できます。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var fruits = new[]
        {
            new { Name = "apple", Color = "red" },
            new { Name = "banana", Color = "yellow" },
            new { Name = "cherry", Color = "red" },
            new { Name = "lemon", Color = "yellow" }
        };
        var lookup = fruits.ToLookup(f => f.Color);
        foreach (var group in lookup)
        {
            Console.WriteLine($"Color: {group.Key}");
            foreach (var fruit in group)
            {
                Console.WriteLine($"  {fruit.Name}");
            }
        }
    }
}
Color: red
  apple
  cherry
Color: yellow
  banana
  lemon

Lookupはキーでグループ化されたデータを高速に検索したい場合に便利です。

DataTableとDataSet

DataTableDataSetはADO.NETの代表的なデータ構造で、データベースから取得した表形式のデータを扱います。

LINQ to DataSetを使うことで、これらのデータに対してLINQクエリを記述できます。

DataTableの行はDataRowCollectionとして保持されており、AsEnumerable()拡張メソッドを使うことでLINQのIEnumerable<DataRow>に変換できます。

using System;
using System.Data;
using System.Linq;
class Program
{
    static void Main()
    {
        var table = new DataTable();
        table.Columns.Add("Id", typeof(int));
        table.Columns.Add("Name", typeof(string));
        table.Columns.Add("Age", typeof(int));
        table.Rows.Add(1, "Alice", 30);
        table.Rows.Add(2, "Bob", 25);
        table.Rows.Add(3, "Charlie", 35);
        var query = table.AsEnumerable()
            .Where(row => row.Field<int>("Age") > 28)
            .OrderBy(row => row.Field<string>("Name"))
            .Select(row => new
            {
                Id = row.Field<int>("Id"),
                Name = row.Field<string>("Name"),
                Age = row.Field<int>("Age")
            });
        foreach (var item in query)
        {
            Console.WriteLine($"{item.Id}: {item.Name} ({item.Age})");
        }
    }
}
1: Alice (30)
3: Charlie (35)

DataSetは複数のDataTableを保持でき、テーブル間のリレーションも管理可能です。

LINQで複数テーブルを結合する場合は、DataTable.AsEnumerable()を使ってそれぞれのテーブルをLINQで操作し、Joinなどの演算子で結合します。

XMLとXDocument

LINQ to XMLは、XMLドキュメントをオブジェクトとして扱い、LINQで簡単に検索や編集ができる機能です。

XDocumentXElementを使ってXMLを読み込み、要素や属性をクエリできます。

using System;
using System.Linq;
using System.Xml.Linq;
class Program
{
    static void Main()
    {
        string xml = @"
            <Books>
                <Book Id='1' Title='C#入門' Price='3000' />
                <Book Id='2' Title='LINQ実践' Price='3500' />
                <Book Id='3' Title='ASP.NET Core' Price='4000' />
            </Books>";
        var doc = XDocument.Parse(xml);
        var expensiveBooks = doc.Descendants("Book")
            .Where(b => (int)b.Attribute("Price") > 3200)
            .Select(b => new
            {
                Id = (int)b.Attribute("Id"),
                Title = (string)b.Attribute("Title"),
                Price = (int)b.Attribute("Price")
            });
        foreach (var book in expensiveBooks)
        {
            Console.WriteLine($"{book.Id}: {book.Title} - {book.Price}円");
        }
    }
}
2: LINQ実践 - 3500円
3: ASP.NET Core - 4000円

XDocumentはXMLの読み書きに便利で、LINQの柔軟なクエリ機能と組み合わせて使うことで、複雑なXML構造も簡単に操作できます。

JSONと匿名型

JSONデータは.NET標準のSystem.Text.JsonNewtonsoft.Jsonなどのライブラリでパースし、LINQで操作可能なオブジェクトに変換できます。

匿名型を使うことで、必要なプロパティだけを抽出して扱いやすくできます。

以下はSystem.Text.Jsonを使った例です。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
class Program
{
    static void Main()
    {
        string json = @"
        [
            { ""Id"": 1, ""Name"": ""Alice"", ""Age"": 30 },
            { ""Id"": 2, ""Name"": ""Bob"", ""Age"": 25 },
            { ""Id"": 3, ""Name"": ""Charlie"", ""Age"": 35 }
        ]";
        var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
        var people = JsonSerializer.Deserialize<List<Person>>(json, options);
        var adults = people
            .Where(p => p.Age >= 30)
            .Select(p => new { p.Name, p.Age });
        foreach (var person in adults)
        {
            Console.WriteLine($"{person.Name} ({person.Age}歳)");
        }
    }
    class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Age { get; set; }
    }
}
Alice (30歳)
Charlie (35歳)

匿名型を使うことで、必要な情報だけを抽出し、型安全に扱えます。

JSONの動的な構造に対してはJsonDocumentJObjectを使い、LINQでクエリをかける方法もあります。

Entity FrameworkとLINQ to Entities

Entity Framework(EF)はORM(Object-Relational Mapper)で、データベースのテーブルをC#のクラスとして扱い、LINQ to Entitiesを使ってデータベースクエリを記述できます。

Entity Frameworkのインストール

Entity Frameworkは、Nugetからインストールする必要があります。

「Entity Framework」と検索してインストールするようにしてください。

dotnet add package Microsoft.EntityFrameworkCore

また、下記のサンプルではIn-Memory データベースを使用しているため、Microsoft.EntityFrameworkCore.InMemoryのインストールも必要です。

dotnet add package Microsoft.EntityFrameworkCore.InMemory

EFはIQueryable<T>を返すため、LINQのクエリはSQLに変換されてデータベース側で実行されます。

using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;

class Program
{
    static void Main()
    {
        using var context = new SampleContext();

        // データをシード
        if (!context.Products.Any())
        {
            context.Products.AddRange(
                new Product { Name = "ProductA", Price = 1500 },
                new Product { Name = "ProductB", Price = 2000 },
                new Product { Name = "ProductC", Price = 800 }
            );
            context.SaveChanges();
        }

        // 価格が1000円を超える商品を名前順に取得
        var expensiveProducts = context.Products
            .Where(p => p.Price > 1000)
            .OrderBy(p => p.Name)
            .Select(p => new { p.Name, p.Price });

        // 結果を出力
        foreach (var product in expensiveProducts)
        {
            Console.WriteLine($"{product.Name}: {product.Price}円");
        }
    }
}

public class SampleContext : DbContext
{
    public DbSet<Product> Products { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseInMemoryDatabase("SampleDB");
}

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Price { get; set; }
}
ProductA: 1500円
ProductB: 2000円

EFはLINQのクエリをSQLに変換し、必要なデータだけを効率的に取得します。

Includeメソッドで関連エンティティの読み込みも可能です。

Dapper・Micro ORMとの併用

Dapperは軽量なMicro ORMで、SQLを直接記述しつつ、結果をC#オブジェクトにマッピングします。

Dapper自体はLINQを提供しませんが、取得したコレクションに対してLINQを使うことが一般的です。

using System;
using System.Data.SQLite;
using System.Linq;
using Dapper;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        using var connection = new SQLiteConnection("Data Source=:memory:");
        connection.Open();
        connection.Execute("CREATE TABLE Users (Id INTEGER PRIMARY KEY, Name TEXT, Age INTEGER)");
        connection.Execute("INSERT INTO Users (Name, Age) VALUES ('Alice', 30), ('Bob', 25), ('Charlie', 35)");
        var users = connection.Query<User>("SELECT * FROM Users").ToList();
        var adults = users.Where(u => u.Age >= 30);
        foreach (var user in adults)
        {
            Console.WriteLine($"{user.Name} ({user.Age}歳)");
        }
    }
}
class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}
Alice (30歳)
Charlie (35歳)

DapperはSQLの自由度が高く、パフォーマンスも優れています。

LINQはDapperで取得したデータの後処理やフィルタリングに活用されることが多いです。

Micro ORMの中にはLINQクエリをサポートするものもありますが、DapperはシンプルにSQLとLINQを組み合わせるスタイルが主流です。

基本操作

LINQの基本操作は、データの抽出や変換、並べ替え、グループ化、集合演算、結合、集計など多岐にわたります。

ここでは代表的な操作を具体的なコード例とともに詳しく解説いたします。

フィルタリング

Whereの使い方

Whereは条件に合致する要素だけを抽出するための演算子です。

引数に条件を表すラムダ式を渡します。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = Enumerable.Range(1, 10);
        // 偶数だけ抽出
        var evens = numbers.Where(n => n % 2 == 0);
        foreach (var n in evens)
        {
            Console.WriteLine(n);
        }
    }
}
2
4
6
8
10

Whereは遅延実行されるため、列挙時に条件が評価されます。

インデックス付きWhere

Whereには要素の値だけでなく、インデックスも受け取るオーバーロードがあります。

これを使うと、要素の位置に基づく条件も指定可能です。

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

投影

Selectと匿名型

Selectは要素を別の形に変換する演算子です。

匿名型と組み合わせることで、必要なプロパティだけを抽出したり、新しいオブジェクトを作成したりできます。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var people = new[]
        {
            new { Name = "Alice", Age = 30, City = "Tokyo" },
            new { Name = "Bob", Age = 25, City = "Osaka" },
            new { Name = "Charlie", Age = 35, City = "Nagoya" }
        };
        var namesAndAges = people.Select(p => new { p.Name, p.Age });
        foreach (var item in namesAndAges)
        {
            Console.WriteLine($"{item.Name} ({item.Age}歳)");
        }
    }
}
Alice (30歳)
Bob (25歳)
Charlie (35歳)

SelectManyによるフラット化

SelectManyは、各要素から複数の要素を生成し、それらを一つのシーケンスに平坦化します。

ネストされたコレクションを扱う際に便利です。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var students = new[]
        {
            new { Name = "Alice", Scores = new[] { 80, 90 } },
            new { Name = "Bob", Scores = new[] { 70, 85, 88 } },
            new { Name = "Charlie", Scores = new[] { 95 } }
        };
        // 全てのスコアを一つのシーケンスにまとめる
        var allScores = students.SelectMany(s => s.Scores);
        foreach (var score in allScores)
        {
            Console.WriteLine(score);
        }
    }
}
80
90
70
85
88
95

並べ替え

OrderByとOrderByDescending

OrderByは昇順、OrderByDescendingは降順で要素を並べ替えます。

キーを指定するラムダ式を渡します。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = new[] { 5, 2, 9, 1, 6 };
        var ascending = numbers.OrderBy(n => n);
        var descending = numbers.OrderByDescending(n => n);
        Console.WriteLine("昇順:");
        foreach (var n in ascending) Console.WriteLine(n);
        Console.WriteLine("降順:");
        foreach (var n in descending) Console.WriteLine(n);
    }
}
昇順:
1
2
5
6
9
降順:
9
6
5
2
1

複合キーによる並べ替え

複数のキーで並べ替えたい場合は、ThenByThenByDescendingを使います。

using System;
using System.Linq;
class Program
{
    class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public string City { get; set; }
    }
    static void Main()
    {
        var people = new[]
        {
            new Person { Name = "Alice", Age = 30, City = "Tokyo" },
            new Person { Name = "Bob", Age = 25, City = "Osaka" },
            new Person { Name = "Charlie", Age = 30, City = "Osaka" },
            new Person { Name = "David", Age = 25, City = "Tokyo" }
        };
        var sorted = people
            .OrderBy(p => p.Age)
            .ThenBy(p => p.City);
        foreach (var p in sorted)
        {
            Console.WriteLine($"{p.Name}: {p.Age}歳, {p.City}");
        }
    }
}
Bob: 25歳, Osaka
David: 25歳, Tokyo
Charlie: 30歳, Osaka
Alice: 30歳, Tokyo

グループ化

GroupBy基本形

GroupByは指定したキーで要素をグループ化します。

戻り値はキーとグループ化された要素のコレクションです。

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

先頭要素抽出と集計

グループごとに先頭要素を取得したり、集計を行うこともよくあります。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var people = new[]
        {
            new { Name = "Alice", City = "Tokyo", Age = 30 },
            new { Name = "Bob", City = "Osaka", Age = 25 },
            new { Name = "Charlie", City = "Tokyo", Age = 35 },
            new { Name = "David", City = "Osaka", Age = 40 }
        };
        var grouped = people.GroupBy(p => p.City)
            .Select(g => new
            {
                City = g.Key,
                Count = g.Count(),
                Oldest = g.OrderByDescending(p => p.Age).First()
            });
        foreach (var group in grouped)
        {
            Console.WriteLine($"{group.City}: {group.Count}人, 最年長は{group.Oldest.Name}({group.Oldest.Age}歳)");
        }
    }
}
Tokyo: 2人, 最年長はCharlie(35歳)
Osaka: 2人, 最年長はDavid(40歳)

集合演算

Distinctで重複排除

Distinctはシーケンス内の重複要素を取り除きます。

既定ではEqualsGetHashCodeで比較します。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = new[] { 1, 2, 2, 3, 4, 4, 5 };
        var distinct = numbers.Distinct();
        foreach (var n in distinct)
        {
            Console.WriteLine(n);
        }
    }
}
1
2
3
4
5

Union・Intersect・Except

これらは集合演算を行います。

  • Union: 2つのシーケンスの和集合(重複なし)
  • Intersect: 2つのシーケンスの共通部分
  • Except: 1つ目のシーケンスから2つ目のシーケンスの要素を除外
using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var set1 = new[] { 1, 2, 3, 4 };
        var set2 = new[] { 3, 4, 5, 6 };
        var union = set1.Union(set2);
        var intersect = set1.Intersect(set2);
        var except = set1.Except(set2);
        Console.WriteLine("Union:");
        foreach (var n in union) Console.WriteLine(n);
        Console.WriteLine("Intersect:");
        foreach (var n in intersect) Console.WriteLine(n);
        Console.WriteLine("Except:");
        foreach (var n in except) Console.WriteLine(n);
    }
}
Union:
1
2
3
4
5
6
Intersect:
3
4
Except:
1
2

結合

内部結合Join

Joinは2つのシーケンスをキーで結合し、共通のキーを持つ要素の組み合わせを生成します。

using System;
using System.Linq;
class Program
{
    class Employee
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
    class Department
    {
        public int EmployeeId { get; set; }
        public string DeptName { get; set; }
    }
    static void Main()
    {
        var employees = new[]
        {
            new Employee { Id = 1, Name = "Alice" },
            new Employee { Id = 2, Name = "Bob" },
            new Employee { Id = 3, Name = "Charlie" }
        };
        var departments = new[]
        {
            new Department { EmployeeId = 1, DeptName = "HR" },
            new Department { EmployeeId = 2, DeptName = "IT" }
        };
        var query = employees.Join(
            departments,
            e => e.Id,
            d => d.EmployeeId,
            (e, d) => new { e.Name, d.DeptName }
        );
        foreach (var item in query)
        {
            Console.WriteLine($"{item.Name}: {item.DeptName}");
        }
    }
}
Alice: HR
Bob: IT

左外部結合GroupJoinとDefaultIfEmpty

GroupJoinは左外部結合を実現し、結合先がない場合は空のコレクションになります。

DefaultIfEmptyを使うと、結合先がない場合にデフォルト値を返せます。

using System;
using System.Linq;
class Program
{
    class Employee
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
    class Department
    {
        public int EmployeeId { get; set; }
        public string DeptName { get; set; }
    }
    static void Main()
    {
        var employees = new[]
        {
            new Employee { Id = 1, Name = "Alice" },
            new Employee { Id = 2, Name = "Bob" },
            new Employee { Id = 3, Name = "Charlie" }
        };
        var departments = new[]
        {
            new Department { EmployeeId = 1, DeptName = "HR" },
            new Department { EmployeeId = 2, DeptName = "IT" }
        };
        var query = employees.GroupJoin(
            departments,
            e => e.Id,
            d => d.EmployeeId,
            (e, ds) => new { e.Name, Depts = ds.DefaultIfEmpty() }
        );
        foreach (var item in query)
        {
            foreach (var dept in item.Depts)
            {
                var deptName = dept?.DeptName ?? "なし";
                Console.WriteLine($"{item.Name}: {deptName}");
            }
        }
    }
}
Alice: HR
Bob: IT
Charlie: なし

クロス結合SelectMany

SelectManyは2つのシーケンスの全組み合わせ(クロス結合)を作成できます。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var colors = new[] { "Red", "Green" };
        var shapes = new[] { "Circle", "Square" };
        var crossJoin = colors.SelectMany(
            color => shapes,
            (color, shape) => $"{color} {shape}"
        );
        foreach (var item in crossJoin)
        {
            Console.WriteLine(item);
        }
    }
}
Red Circle
Red Square
Green Circle
Green Square

集計

Count・LongCount

Countは要素数を返します。

LongCountは要素数が非常に多い場合に64ビット整数で返します。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = Enumerable.Range(1, 100);
        Console.WriteLine($"Count: {numbers.Count()}");
        Console.WriteLine($"LongCount: {numbers.LongCount()}");
    }
}
Count: 100
LongCount: 100

Sum・Average

Sumは数値の合計、Averageは平均値を計算します。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = new[] { 1, 2, 3, 4, 5 };
        Console.WriteLine($"Sum: {numbers.Sum()}");
        Console.WriteLine($"Average: {numbers.Average()}");
    }
}
Sum: 15
Average: 3

Min・Max

Minは最小値、Maxは最大値を返します。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = new[] { 10, 20, 5, 30, 15 };
        Console.WriteLine($"Min: {numbers.Min()}");
        Console.WriteLine($"Max: {numbers.Max()}");
    }
}
Min: 5
Max: 30

カスタム集計Aggregate

Aggregateはシーケンスの要素を累積的に処理し、カスタム集計を行えます。

例えば、文字列の連結や複雑な計算に使います。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var words = new[] { "LINQ", "is", "powerful" };
        var sentence = words.Aggregate((acc, word) => acc + " " + word);
        Console.WriteLine(sentence);
    }
}
LINQ is powerful

Aggregateは初期値を指定するオーバーロードもあり、より複雑な処理も可能です。

コントロールフロー系演算子

LINQには、データの取得範囲を制限したり、条件に基づいて要素を選択したり、特定の要素を取得するためのコントロールフロー系演算子が用意されています。

ここでは代表的な演算子の使い方や特徴を具体例とともに解説いたします。

取得制限

Take・Skip

Takeはシーケンスの先頭から指定した数だけ要素を取得します。

一方、Skipは先頭から指定した数の要素をスキップし、それ以降の要素を取得します。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = Enumerable.Range(1, 10);
        var firstThree = numbers.Take(3);
        var skipThree = numbers.Skip(3);
        Console.WriteLine("Take(3):");
        foreach (var n in firstThree)
        {
            Console.WriteLine(n);
        }
        Console.WriteLine("Skip(3):");
        foreach (var n in skipThree)
        {
            Console.WriteLine(n);
        }
    }
}
Take(3):
1
2
3
Skip(3):
4
5
6
7
8
9
10

TakeSkipは組み合わせて使うことで、データの一部を抽出したり、ページング処理に活用できます。

ページング実装例

ページングは大量データを分割して表示する際に使います。

Skipで前のページ分をスキップし、Takeでページサイズ分だけ取得します。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var items = Enumerable.Range(1, 50);
        int pageSize = 10;
        int pageNumber = 3; // 3ページ目(0ベースではなく1ベース)
        var pageItems = items
            .Skip((pageNumber - 1) * pageSize)
            .Take(pageSize);
        Console.WriteLine($"Page {pageNumber}:");
        foreach (var item in pageItems)
        {
            Console.WriteLine(item);
        }
    }
}
Page 3:
21
22
23
24
25
26
27
28
29
30

このようにSkipTakeを組み合わせることで、簡単にページング処理が実装できます。

条件系

TakeWhile・SkipWhile

TakeWhileは条件が真の間、先頭から要素を取得し続けます。

条件が初めて偽になった時点で取得を停止します。

SkipWhileは条件が真の間、先頭から要素をスキップし、条件が偽になった以降の要素を取得します。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = new[] { 2, 4, 6, 7, 8, 10 };
        var takeWhileEven = numbers.TakeWhile(n => n % 2 == 0);
        var skipWhileEven = numbers.SkipWhile(n => n % 2 == 0);
        Console.WriteLine("TakeWhile (偶数の間):");
        foreach (var n in takeWhileEven)
        {
            Console.WriteLine(n);
        }
        Console.WriteLine("SkipWhile (偶数の間スキップ):");
        foreach (var n in skipWhileEven)
        {
            Console.WriteLine(n);
        }
    }
}
TakeWhile (偶数の間):
2
4
6
SkipWhile (偶数の間スキップ):
7
8
10

TakeWhileは条件が途切れたら終了するため、途中に条件を満たさない要素があるとそこで止まります。

SkipWhileは条件が途切れたら以降すべて取得します。

Any・All・Contains

  • Anyはシーケンスに条件を満たす要素が1つでもあればtrueを返します。条件を省略すると要素が1つでもあればtrueです
  • Allはすべての要素が条件を満たす場合にtrueを返します
  • Containsは指定した値がシーケンスに含まれているか判定します
using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = new[] { 1, 3, 5, 7, 9 };
        bool anyEven = numbers.Any(n => n % 2 == 0);
        bool allOdd = numbers.All(n => n % 2 != 0);
        bool containsFive = numbers.Contains(5);
        Console.WriteLine($"Any even? {anyEven}");
        Console.WriteLine($"All odd? {allOdd}");
        Console.WriteLine($"Contains 5? {containsFive}");
    }
}
Any even? False
All odd? True
Contains 5? True

要素取得

First・FirstOrDefault

Firstは条件に合う最初の要素を返します。

条件を省略すると最初の要素を返します。

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

FirstOrDefaultは条件に合う要素がなければ既定値(参照型ならnull、値型ならdefault)を返します。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = new[] { 2, 4, 6, 8 };
        int firstEven = numbers.First(n => n % 2 == 0);
        int firstGreaterThanTen = numbers.FirstOrDefault(n => n > 10);
        Console.WriteLine($"First even: {firstEven}");
        Console.WriteLine($"First > 10 or default: {firstGreaterThanTen}");
    }
}
First even: 2
First > 10 or default: 0

Single・SingleOrDefault

Singleは条件に合う要素がちょうど1つの場合にその要素を返します。

0個または複数ある場合は例外が発生します。

SingleOrDefaultは条件に合う要素が0個の場合は既定値を返し、複数ある場合は例外が発生します。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = new[] { 5 };
        int single = numbers.Single();
        int singleOrDefault = numbers.SingleOrDefault(n => n > 10);
        Console.WriteLine($"Single: {single}");
        Console.WriteLine($"SingleOrDefault (条件に合わず): {singleOrDefault}");
    }
}
Single: 5
SingleOrDefault (条件に合わず): 0

複数要素がある場合にSingleを使うと例外になるため、ユニークな要素を取得したい場合に使います。

ElementAt

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

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

ElementAtOrDefaultは範囲外の場合に既定値を返します。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var letters = new[] { "a", "b", "c" };
        string second = letters.ElementAt(1);
        string fifthOrDefault = letters.ElementAtOrDefault(4);
        Console.WriteLine($"ElementAt(1): {second}");
        Console.WriteLine($"ElementAtOrDefault(4): {(fifthOrDefault ?? "null")}");
    }
}
ElementAt(1): b
ElementAtOrDefault(4): null

例外発生タイミングの違い

  • FirstSingleは、条件に合う要素がない場合や複数ある場合に例外をスローします
  • FirstOrDefaultSingleOrDefaultは例外をスローせず、既定値を返します
  • ElementAtはインデックスが範囲外の場合に例外をスローし、ElementAtOrDefaultは既定値を返します

これらの違いを理解し、用途に応じて使い分けることが重要です。

例えば、必ず要素が存在するとわかっている場合はFirstSingleを使い、存在しない可能性がある場合はFirstOrDefaultSingleOrDefaultを使うと安全です。

生成演算子

LINQには、特定のパターンでシーケンスを生成するための便利な演算子が用意されています。

ここではRangeRepeatEmptyの使い方と、遅延実行されるシーケンスの自作方法について解説いたします。

Range・Repeat・Empty

Range

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

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

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = Enumerable.Range(1, 10);
        foreach (var n in numbers)
        {
            Console.WriteLine(n);
        }
    }
}
1
2
3
4
5
6
7
8
9
10

Rangeは開始値と要素数を指定し、連続した整数を簡単に作成できるため、テストデータの生成やループの代替として便利です。

Repeat

Enumerable.Repeatは、指定した値を指定回数繰り返すシーケンスを生成します。

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

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var repeated = Enumerable.Repeat("Hello", 5);
        foreach (var s in repeated)
        {
            Console.WriteLine(s);
        }
    }
}
Hello
Hello
Hello
Hello
Hello

Repeatは同じ値を複数回使いたい場合に便利で、初期化処理やデフォルト値の埋め込みなどに活用できます。

Empty

Enumerable.Empty<T>は、空のシーケンスを返します。

nullではなく空のシーケンスを返すため、nullチェックを省略できる場面で役立ちます。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var empty = Enumerable.Empty<int>();
        Console.WriteLine($"Count: {empty.Count()}");
    }
}
Count: 0

Emptyはメソッドの戻り値として空のシーケンスを返したい場合や、条件によってシーケンスを返すが要素がない場合の安全な返却値として使います。

遅延系シーケンスの自作

LINQのシーケンスは遅延実行されるため、必要なときに要素を生成するカスタムシーケンスを作成できます。

これにはyield returnを使ったイテレーターメソッドが便利です。

以下は、1から指定した数までの偶数だけを遅延生成する例です。

using System;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        foreach (var even in GenerateEvenNumbers(10))
        {
            Console.WriteLine(even);
        }
    }
    static IEnumerable<int> GenerateEvenNumbers(int max)
    {
        for (int i = 1; i <= max; i++)
        {
            if (i % 2 == 0)
            {
                yield return i; // 遅延生成
            }
        }
    }
}
2
4
6
8
10

yield returnを使うことで、呼び出し元が要素を要求したタイミングで値を1つずつ返すため、メモリ効率が良く、大量データの処理に適しています。

また、無限シーケンスを作ることも可能です。

以下は無限に自然数を生成する例です。

using System;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        foreach (var n in NaturalNumbers())
        {
            if (n > 10) break; // 無限ループ防止
            Console.WriteLine(n);
        }
    }
    static IEnumerable<int> NaturalNumbers()
    {
        int i = 1;
        while (true)
        {
            yield return i++;
        }
    }
}
1
2
3
4
5
6
7
8
9
10

このように遅延系シーケンスの自作は、LINQの柔軟性を最大限に活かすテクニックです。

yield returnを使うことで、複雑な生成ロジックもシンプルに記述できます。

変換演算子

LINQの変換演算子は、クエリの結果を別のコレクション型に変換したり、要素の型を変換したりするために使います。

ここでは代表的なToListToArrayToDictionaryToLookup、および型変換のCastOfTypeについて詳しく解説いたします。

ToList・ToArray

ToListToArrayは、LINQの遅延実行されるシーケンスを即時実行し、結果をList<T>や配列T[]に変換します。

これにより、結果を複数回列挙したり、インデックスアクセスを高速に行ったりできます。

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

ToListは可変長のコレクションとして扱いたい場合に便利で、AddRemoveなどの操作が可能です。

ToArrayは固定長の配列として扱いたい場合に使います。

どちらも即時実行されるため、遅延実行のクエリを一度評価して結果をメモリに保持したいときに使います。

ToDictionary・ToLookup

ToDictionary

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

キーの重複があると例外が発生するため、キーはユニークである必要があります。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
    static void Main()
    {
        var people = new[]
        {
            new Person { Id = 1, Name = "Alice" },
            new Person { Id = 2, Name = "Bob" },
            new Person { Id = 3, Name = "Charlie" }
        };
        Dictionary<int, string> dict = people.ToDictionary(p => p.Id, p => p.Name);
        foreach (var kv in dict)
        {
            Console.WriteLine($"Key: {kv.Key}, Value: {kv.Value}");
        }
    }
}
Key: 1, Value: Alice
Key: 2, Value: Bob
Key: 3, Value: Charlie

ToDictionaryはキーと値の選択子をラムダ式で指定でき、複雑な変換も可能です。

ToLookup

ToLookupはキーに対して複数の要素をグループ化したILookup<TKey, TElement>を生成します。

キーの重複を許容し、グループ化されたコレクションとして扱えます。

using System;
using System.Linq;
class Program
{
    class Fruit
    {
        public string Name { get; set; }
        public string Color { get; set; }
    }
    static void Main()
    {
        var fruits = new[]
        {
            new Fruit { Name = "Apple", Color = "Red" },
            new Fruit { Name = "Strawberry", Color = "Red" },
            new Fruit { Name = "Banana", Color = "Yellow" },
            new Fruit { Name = "Lemon", Color = "Yellow" }
        };
        var lookup = fruits.ToLookup(f => f.Color);
        foreach (var group in lookup)
        {
            Console.WriteLine($"Color: {group.Key}");
            foreach (var fruit in group)
            {
                Console.WriteLine($"  {fruit.Name}");
            }
        }
    }
}
Color: Red
  Apple
  Strawberry
Color: Yellow
  Banana
  Lemon

ToLookupはグループ化の結果を保持し、キーで高速にアクセスしたい場合に便利です。

データ型の変換Cast・OfType

Cast

Cast<TResult>は、非ジェネリックなIEnumerable(例えばArrayListIEnumerable型)をジェネリックなIEnumerable<TResult>に変換します。

要素が指定した型にキャスト可能でなければ例外が発生します。

using System;
using System.Collections;
using System.Linq;
class Program
{
    static void Main()
    {
        ArrayList list = new ArrayList { 1, 2, 3, 4 };
        var casted = list.Cast<int>();
        foreach (var n in casted)
        {
            Console.WriteLine(n);
        }
    }
}
1
2
3
4

Castは型が確実に合っている場合に使います。

OfType

OfType<TResult>は、シーケンス内の指定した型の要素だけを抽出し、IEnumerable<TResult>として返します。

型が合わない要素は無視されます。

using System;
using System.Collections;
using System.Linq;
class Program
{
    static void Main()
    {
        ArrayList list = new ArrayList { 1, "two", 3, "four", 5 };
        var ints = list.OfType<int>();
        foreach (var n in ints)
        {
            Console.WriteLine(n);
        }
    }
}
1
3
5

OfTypeは混在したコレクションから特定の型だけを安全に抽出したい場合に便利です。

これらの変換演算子を使いこなすことで、LINQの結果を目的に応じたコレクション型に変換し、柔軟かつ効率的にデータを扱えます。

特にToDictionaryToLookupはキーによる高速アクセスやグループ化に役立ちますし、CastOfTypeは型安全な操作を支援します。

パフォーマンス最適化

LINQは強力で便利な機能ですが、使い方によってはパフォーマンスに影響を与えることがあります。

ここでは遅延実行と即時実行の違いや、多重列挙の回避、メモリとCPUのトレードオフ、並列処理のPLINQ、そしてキャッシュと再利用のテクニックについて詳しく解説いたします。

遅延実行と即時実行

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

つまり、クエリを定義しただけでは処理は行われず、実際に列挙(foreachなど)したタイミングで初めて処理が実行されます。

一方、ToListToArrayなどの変換演算子を使うと即時実行され、結果がメモリに格納されます。

ToListの使いどころ

ToListは遅延実行のクエリを即時実行し、結果をList<T>としてメモリに保持します。

これにより、複数回の列挙で同じクエリを繰り返し実行することを防ぎ、パフォーマンスを向上させることができます。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = Enumerable.Range(1, 5);
        // 遅延実行のままだとforeachごとに処理が走る
        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 cached = numbers.Where(x => x % 2 == 0).ToList();
        Console.WriteLine("ToListでキャッシュ:");
        foreach (var n in cached)
            Console.WriteLine(n);
        foreach (var n in cached)
            Console.WriteLine(n);
    }
}
遅延実行:
2
4
2
4
ToListでキャッシュ:
2
4
2
4

上記の例では結果は同じですが、遅延実行の場合はWhereの条件が2回評価されます。

ToListを使うと1回だけ評価し、以降はメモリ上のリストを使うため効率的です。

ただし、ToListは結果をすべてメモリに保持するため、大量データの場合はメモリ使用量に注意が必要です。

意図しない多重列挙の回避

遅延実行のクエリを複数回列挙すると、そのたびにクエリが再評価されます。

これが意図しない多重列挙で、パフォーマンス低下や副作用の原因になることがあります。

using System;
using System.Linq;
class Program
{
    static int counter = 0;
    static void Main()
    {
        var numbers = Enumerable.Range(1, 3).Select(n =>
        {
            counter++;
            return n;
        });
        // 2回列挙するとSelectの処理が2回走る
        foreach (var n in numbers)
            Console.WriteLine(n);
        foreach (var n in numbers)
            Console.WriteLine(n);
        Console.WriteLine($"処理回数: {counter}");
    }
}
1
2
3
1
2
3
処理回数: 6

このように、2回列挙したためSelectの処理が6回実行されています。

これを防ぐにはToListToArrayで結果をキャッシュするか、Memoizeパターンを使います。

メモリとCPUのトレードオフ

LINQのパフォーマンス最適化では、メモリ使用量とCPU負荷のバランスを考慮する必要があります。

  • 遅延実行は必要な要素だけを処理するためメモリ効率が良いですが、クエリが複数回評価されるとCPU負荷が増えます
  • 即時実行(ToListなど)は一度にすべての結果をメモリに保持するため、CPU負荷は減りますがメモリ使用量が増えます

大量データを扱う場合は、必要な部分だけを遅延実行で処理しつつ、複数回使う結果はキャッシュするなど、状況に応じて使い分けることが重要です。

PLINQによる並列化

PLINQ(Parallel LINQ)は、LINQクエリを並列処理に変換し、複数のCPUコアを活用して高速化を図る機能です。

AsParallelメソッドを使ってシーケンスを並列化します。

AsParallelとスケジューラ

using System;
using System.Linq;
using System.Threading;
class Program
{
    static void Main()
    {
        var numbers = Enumerable.Range(1, 20);
        var parallelQuery = numbers.AsParallel()
            .Select(n =>
            {
                Console.WriteLine($"Processing {n} on thread {Thread.CurrentThread.ManagedThreadId}");
                return n * n;
            });
        foreach (var result in parallelQuery)
        {
            Console.WriteLine(result);
        }
    }
}
Processing 1 on thread 4
Processing 2 on thread 5
Processing 3 on thread 6
...
1
4
9
...

AsParallelを使うと、LINQの処理が複数スレッドで並列に実行されます。

PLINQは内部でスケジューラを使い、CPUコア数に応じてスレッドを管理します。

ただし、並列化によるオーバーヘッドやスレッド間の競合が発生するため、必ずしも高速化するとは限りません。

処理内容やデータ量に応じて使い分ける必要があります。

スレッドセーフな書き方

PLINQで並列処理を行う際は、スレッドセーフでない操作を避ける必要があります。

例えば、共有変数への書き込みや非同期I/Oは注意が必要です。

using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
class Program
{
    static void Main()
    {
        var numbers = Enumerable.Range(1, 1000);
        var results = new ConcurrentBag<int>();
        numbers.AsParallel().ForAll(n =>
        {
            // ConcurrentBagはスレッドセーフ
            results.Add(n * n);
        });
        Console.WriteLine($"結果数: {results.Count}");
    }
}
結果数: 1000

ConcurrentBag<T>などのスレッドセーフなコレクションを使うことで、並列処理中のデータ競合を防げます。

キャッシュと再利用

Memoizeパターン

Memoizeは、遅延実行のシーケンスの結果を一度だけ評価し、その後はキャッシュされた結果を返すパターンです。

これにより、多重列挙による無駄な処理を防ぎます。

以下は簡単なMemoizeの実装例です。

using System;
using System.Collections.Generic;
public static class EnumerableExtensions
{
    public static IEnumerable<T> Memoize<T>(this IEnumerable<T> source)
    {
        var cache = new List<T>();
        bool fullyEnumerated = false;
        foreach (var item in source)
        {
            cache.Add(item);
            yield return item;
        }
        fullyEnumerated = true;
        int index = 0;
        while (true)
        {
            if (index < cache.Count)
            {
                yield return cache[index++];
            }
            else if (fullyEnumerated)
            {
                yield break;
            }
        }
    }
}
class Program
{
    static void Main()
    {
        var numbers = GenerateNumbers();
        var memoized = numbers.Memoize();
        foreach (var n in memoized)
            Console.WriteLine(n);
        Console.WriteLine("再列挙:");
        foreach (var n in memoized)
            Console.WriteLine(n);
    }
    static IEnumerable<int> GenerateNumbers()
    {
        Console.WriteLine("生成開始");
        for (int i = 1; i <= 3; i++)
        {
            Console.WriteLine($"生成中: {i}");
            yield return i;
        }
    }
}
生成開始
生成中: 1
1
生成中: 2
2
生成中: 3
3
再列挙:
1
2
3

この例では、Memoizeを使うことで最初の列挙時に生成した結果をキャッシュし、2回目以降の列挙ではキャッシュから返しています。

これにより、生成処理の重複を防げます。

パフォーマンス最適化では、遅延実行の特性を理解し、必要に応じて即時実行やキャッシュを使い分けることが重要です。

また、PLINQを活用して並列化を行う際はスレッドセーフな設計を心がけ、メモリとCPUのバランスを考慮しながら最適な方法を選択しましょう。

エラーと例外処理

LINQを使う際には、データの不整合や型の不一致、null参照などによるエラーや例外に注意が必要です。

ここでは、Null許容の安全なクエリの書き方、OfTypeを使った型不一致の回避、そしてTry/Catchと遅延実行の関係による落とし穴について詳しく解説いたします。

Null許容の安全なクエリ

LINQクエリでnull値を含むデータを扱う場合、null参照例外NullReferenceExceptionが発生しやすくなります。

特に、オブジェクトのプロパティにアクセスする際はnullチェックを適切に行うことが重要です。

以下はnull許容の安全なクエリの例です。

using System;
using System.Linq;
class Program
{
    class Person
    {
        public string Name { get; set; }
        public string City { get; set; }
    }
    static void Main()
    {
        var people = new[]
        {
            new Person { Name = "Alice", City = "Tokyo" },
            new Person { Name = "Bob", City = null },
            new Person { Name = "Charlie", City = "Osaka" },
            null
        };
        // nullチェックを含めた安全なクエリ
        var filtered = people
            .Where(p => p != null && !string.IsNullOrEmpty(p.City))
            .Select(p => p.Name + " lives in " + p.City);
        foreach (var s in filtered)
        {
            Console.WriteLine(s);
        }
    }
}
Alice lives in Tokyo
Charlie lives in Osaka

この例では、p != nullでnullオブジェクトを除外し、!string.IsNullOrEmpty(p.City)でCityプロパティがnullまたは空文字の要素を除外しています。

これにより、null参照例外を防ぎつつ安全にクエリを実行できます。

また、C# 6.0以降のnull条件演算子?.を使うと、より簡潔にnullチェックが可能です。

var filtered = people
    .Where(p => !string.IsNullOrEmpty(p?.City))
    .Select(p => p.Name + " lives in " + p.City);

null条件演算子は、左辺がnullの場合にnullを返し、例外を防ぎます。

OfTypeで型不一致を防ぐ

LINQのシーケンスに異なる型のオブジェクトが混在している場合、特定の型だけを抽出したいことがあります。

OfType<T>は指定した型にキャスト可能な要素だけを抽出し、型不一致による例外を防ぎます。

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

OfTypeは内部でis演算子を使って型チェックを行い、キャスト可能な要素だけを返します。

これにより、Cast<T>のように不適切な型が混じっているときに発生するInvalidCastExceptionを回避できます。

Try/Catchと遅延実行の落とし穴

LINQのクエリは遅延実行されるため、Try/Catchでクエリ定義時に例外を捕捉しようとしても、実際の例外は列挙時に発生します。

これにより、例外処理のタイミングを誤ることがあります。

以下の例で説明します。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = new int[] { 1, 2, 0, 4 };
        // 0で割るため例外が発生する可能性があるクエリ
        var query = numbers.Select(n => 10 / n);
        try
        {
            // クエリ定義時は例外は発生しない
            Console.WriteLine("クエリ定義完了");
            // 列挙時に例外が発生する
            foreach (var result in query)
            {
                Console.WriteLine(result);
            }
        }
        catch (DivideByZeroException ex)
        {
            Console.WriteLine("例外をキャッチ: " + ex.Message);
        }
    }
}
クエリ定義完了
10
5
例外をキャッチ: Attempted to divide by zero.

この例では、Selectの中の除算は列挙時に実行されるため、Try/Catchは列挙の部分で例外を捕捉しています。

クエリ定義時には例外は発生しません。

もし例外をクエリ定義時に捕捉したい場合は、ToListToArrayなどで即時実行させる必要があります。

try
{
    var results = numbers.Select(n => 10 / n).ToList(); // 即時実行
}
catch (DivideByZeroException ex)
{
    Console.WriteLine("例外をキャッチ: " + ex.Message);
}

このように、遅延実行の特性を理解し、例外処理のタイミングを適切に設計することが重要です。

特にデータベースアクセスやファイル操作など副作用のある処理をLINQで行う場合は注意が必要です。

カスタム拡張メソッド

LINQの標準クエリ演算子は非常に便利ですが、特定の業務ロジックや再利用したい処理を自分で拡張メソッドとして実装することも可能です。

ここではユーザー定義演算子の作り方、パイプラインの再利用方法、そしてパフォーマンス面での注意点について詳しく解説いたします。

ユーザー定義演算子の作り方

LINQの拡張メソッドは、IEnumerable<T>IQueryable<T>を受け取り、同じくIEnumerable<T>IQueryable<T>を返すメソッドとして実装します。

これにより、メソッドチェーンの一部として自然に組み込めます。

以下は、偶数だけを抽出するカスタム演算子WhereEvenの例です。

using System;
using System.Collections.Generic;
using System.Linq;
public static class LinqExtensions
{
    public static IEnumerable<int> WhereEven(this IEnumerable<int> source)
    {
        if (source == null) throw new ArgumentNullException(nameof(source));
        foreach (var item in source)
        {
            if (item % 2 == 0)
                yield return item;
        }
    }
}
class Program
{
    static void Main()
    {
        var numbers = Enumerable.Range(1, 10);
        var evens = numbers.WhereEven();
        foreach (var n in evens)
        {
            Console.WriteLine(n);
        }
    }
}
2
4
6
8
10

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

  • 拡張メソッドはstaticクラス内のstaticメソッドとして定義し、第一引数にthis修飾子を付けて対象の型を指定します
  • 入力のIEnumerable<T>を遅延実行で処理するためにyield returnを使います
  • nullチェックを行い、引数がnullの場合は例外をスローして安全性を確保します

このようにカスタム演算子を作ることで、業務ロジックに特化したフィルタリングや変換を簡潔に再利用可能な形で提供できます。

パイプラインの再利用

LINQのメソッドチェーンはパイプラインとして表現でき、複数のクエリ処理を組み合わせて再利用することが可能です。

カスタム拡張メソッドを使うことで、共通の処理をまとめてパイプライン化し、コードの重複を減らせます。

例えば、文字列のリストから空文字やnullを除外し、トリムして大文字に変換する共通処理を拡張メソッドとしてまとめます。

using System;
using System.Collections.Generic;
using System.Linq;
public static class LinqExtensions
{
    public static IEnumerable<string> CleanAndUpper(this IEnumerable<string> source)
    {
        if (source == null) throw new ArgumentNullException(nameof(source));
        return source
            .Where(s => !string.IsNullOrWhiteSpace(s))
            .Select(s => s.Trim().ToUpperInvariant());
    }
}
class Program
{
    static void Main()
    {
        var words = new List<string> { " apple ", null, "  ", "banana", "Cherry " };
        var cleaned = words.CleanAndUpper();
        foreach (var word in cleaned)
        {
            Console.WriteLine(word);
        }
    }
}
APPLE
BANANA
CHERRY

このようにパイプラインを拡張メソッドとして切り出すことで、複数の場所で同じ処理を簡単に再利用でき、保守性が向上します。

パフォーマンスへの配慮

カスタム拡張メソッドを作成する際は、パフォーマンスにも注意が必要です。

特に以下の点を意識しましょう。

  • 遅延実行を維持する

yield returnを使い、必要なときに必要な要素だけを生成する遅延実行を維持することで、メモリ使用量を抑えられます。

即時実行のToListToArrayを不用意に使うとメモリ消費が増大します。

  • 引数のnullチェックを行う

早期にArgumentNullExceptionをスローすることで、バグの原因を特定しやすくなります。

  • 不要なコレクションの生成を避ける

例えば、SelectWhereを複数回連続で呼ぶ場合、可能な限り一つのメソッド内で処理をまとめるとオーバーヘッドを減らせます。

  • 複雑な処理は分割してテストしやすく

複雑なロジックを一つの拡張メソッドに詰め込みすぎると可読性やテスト性が低下します。

適切に分割し、単体テストを充実させましょう。

  • 並列処理との相性を考慮する

並列処理(PLINQ)で使う場合は、スレッドセーフな設計が必要です。

状態を持つ処理や副作用のある処理は避けるか、適切に同期を行いましょう。

カスタム拡張メソッドはLINQの柔軟性をさらに高め、業務ロジックの共通化やコードの簡潔化に役立ちます。

パフォーマンスや安全性に配慮しつつ、適切に設計・実装することが重要です。

非同期LINQパターン

非同期プログラミングは、I/O待ちや長時間かかる処理を効率的に扱うために重要です。

LINQでも非同期対応のパターンがあり、特にデータベースアクセスやストリーム処理で活用されます。

ここではAsAsyncEnumerableを使ったストリーム処理と、ToListAsyncを使ったデータベース呼び出しについて詳しく解説いたします。

AsAsyncEnumerableでストリーム処理

AsAsyncEnumerableは、同期的なIQueryableIEnumerableを非同期のIAsyncEnumerable<T>に変換し、非同期ストリームとして処理できるようにします。

これにより、データを逐次的に非同期で取得しながら処理でき、メモリ効率や応答性が向上します。

以下は、Entity Framework CoreのDbSet<T>AsAsyncEnumerableで非同期ストリーム処理する例です。

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;

class Program
{
    static async Task Main()
    {
        using var context = new SampleContext();
        // データベースとシードデータを作成
        context.Database.EnsureCreated();

        // データを非同期ストリームで取得
        await foreach (var product in context.Products.AsAsyncEnumerable())
        {
            Console.WriteLine($"{product.Id}: {product.Name} - {product.Price}円");
        }
    }
}

public class SampleContext : DbContext
{
    public DbSet<Product> Products { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseInMemoryDatabase("SampleDB");

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Product>().HasData(
            new Product { Id = 1, Name = "ProductA", Price = 1500 },
            new Product { Id = 2, Name = "ProductB", Price = 2000 },
            new Product { Id = 3, Name = "ProductC", Price = 1200 }
        );
    }
}

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Price { get; set; }
}
1: ProductA - 1500円
2: ProductB - 2000円
3: ProductC - 1200円

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

  • AsAsyncEnumerableIQueryable<T>IAsyncEnumerable<T>に変換し、await foreachで非同期に逐次処理できます
  • 大量データを一度にメモリに読み込まず、ストリームとして処理できるためメモリ効率が良いです
  • EF Coreの非同期APIと組み合わせて使うことで、データベースアクセスの待機時間を効率的に扱えます

ToListAsyncでデータベース呼び出し

ToListAsyncは、非同期にクエリを実行して結果をリストに格納します。

データベースからのデータ取得時に使われ、UIスレッドのブロックを防ぎつつ、結果を即時取得したい場合に便利です。

以下はEF CoreでToListAsyncを使った例です。

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
class Program
{
    static async Task Main()
    {
        using var context = new SampleContext();

        // データベースとシードデータを作成
        context.Database.EnsureCreated();

        // 非同期にクエリを実行しリストを取得
        List<Product> products = await context.Products
            .Where(p => p.Price > 1300)
            .OrderBy(p => p.Price)
            .ToListAsync();
        foreach (var product in products)
        {
            Console.WriteLine($"{product.Name}: {product.Price}円");
        }
    }
}
public class SampleContext : DbContext
{
    public DbSet<Product> Products { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseInMemoryDatabase("SampleDB");
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Product>().HasData(
            new Product { Id = 1, Name = "ProductA", Price = 1500 },
            new Product { Id = 2, Name = "ProductB", Price = 2000 },
            new Product { Id = 3, Name = "ProductC", Price = 1200 }
        );
    }
}
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Price { get; set; }
}
ProductA: 1500円
ProductB: 2000円

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

  • ToListAsyncはクエリを非同期に実行し、結果をList<T>として取得します
  • UIアプリケーションやWebアプリケーションで、データベースアクセスの待機時間中にスレッドをブロックしないために使います
  • クエリの途中にWhereOrderByなどのLINQ演算子を組み合わせて柔軟に条件指定が可能です

非同期LINQパターンを活用することで、I/O待ちの効率化やメモリ使用量の最適化が可能になります。

AsAsyncEnumerableはストリーム処理に適し、ToListAsyncは結果をまとめて取得したい場合に適しています。

用途に応じて使い分けることが重要です。

実務での活用例

LINQは実務のさまざまなシーンでデータ操作を効率化し、コードの可読性や保守性を高める強力なツールです。

ここでは、ログ分析の抽出と集計、ランキング生成、不正データフィルタリングの具体的な活用例を示します。

ログ分析の抽出と集計

大量のログデータから特定の条件に合致する情報を抽出し、集計する作業はよくある業務です。

LINQを使うと、複雑な条件指定や集計処理を簡潔に記述できます。

以下は、ログエントリのリストからエラーレベルのログを抽出し、日付ごとに件数を集計する例です。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    class LogEntry
    {
        public DateTime Timestamp { get; set; }
        public string Level { get; set; }
        public string Message { get; set; }
    }
    static void Main()
    {
        var logs = new List<LogEntry>
        {
            new LogEntry { Timestamp = DateTime.Parse("2024-06-01 10:00"), Level = "Info", Message = "Start process" },
            new LogEntry { Timestamp = DateTime.Parse("2024-06-01 10:05"), Level = "Error", Message = "NullReferenceException" },
            new LogEntry { Timestamp = DateTime.Parse("2024-06-01 11:00"), Level = "Error", Message = "TimeoutException" },
            new LogEntry { Timestamp = DateTime.Parse("2024-06-02 09:00"), Level = "Info", Message = "Process completed" },
            new LogEntry { Timestamp = DateTime.Parse("2024-06-02 09:30"), Level = "Error", Message = "InvalidOperationException" }
        };
        var errorCountsByDate = logs
            .Where(log => log.Level == "Error")
            .GroupBy(log => log.Timestamp.Date)
            .Select(g => new { Date = g.Key, Count = g.Count() })
            .OrderBy(x => x.Date);
        foreach (var item in errorCountsByDate)
        {
            Console.WriteLine($"{item.Date:yyyy-MM-dd}: {item.Count}件のエラー");
        }
    }
}
2024-06-01: 2件のエラー
2024-06-02: 1件のエラー

この例では、Whereでエラーレベルのログだけを抽出し、GroupByで日付ごとにグループ化、Countで件数を集計しています。

OrderByで日付順に並べ替え、見やすいレポートを作成しています。

ランキング生成

ランキングは売上やスコア、アクセス数などの順位付けに使われます。

LINQの並べ替えや集計機能を活用すると、簡単にランキングを生成できます。

以下は、商品の売上データから売上金額の高い順にランキングを作成する例です。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    class ProductSales
    {
        public string ProductName { get; set; }
        public int Quantity { get; set; }
        public decimal UnitPrice { get; set; }
    }
    static void Main()
    {
        var sales = new List<ProductSales>
        {
            new ProductSales { ProductName = "商品A", Quantity = 10, UnitPrice = 1500m },
            new ProductSales { ProductName = "商品B", Quantity = 5, UnitPrice = 3000m },
            new ProductSales { ProductName = "商品C", Quantity = 20, UnitPrice = 800m }
        };
        var ranking = sales
            .Select(s => new
            {
                s.ProductName,
                TotalSales = s.Quantity * s.UnitPrice
            })
            .OrderByDescending(x => x.TotalSales)
            .Select((x, index) => new
            {
                Rank = index + 1,
                x.ProductName,
                x.TotalSales
            });
        foreach (var item in ranking)
        {
            Console.WriteLine($"{item.Rank}位: {item.ProductName} - 売上 {item.TotalSales:C}");
        }
    }
}
1位: 商品C - 売上 ¥16,000
2位: 商品A - 売上 ¥15,000
3位: 商品B - 売上 ¥15,000

この例では、Selectで売上金額を計算し、OrderByDescendingで降順に並べ替えています。

さらに、Selectのインデックス付きオーバーロードを使って順位を付けています。

不正データフィルタリング

データの中に不正な値や異常値が混入している場合、LINQで条件を指定してフィルタリングし、除外や修正を行うことができます。

以下は、ユーザー情報のリストからメールアドレスが不正なデータを除外する例です。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
class Program
{
    class User
    {
        public string Name { get; set; }
        public string Email { get; set; }
    }
    static void Main()
    {
        var users = new List<User>
        {
            new User { Name = "Alice", Email = "alice@example.com" },
            new User { Name = "Bob", Email = "bob[at]example.com" },
            new User { Name = "Charlie", Email = "charlie@example.com" },
            new User { Name = "David", Email = "david.example.com" }
        };
        var emailPattern = @"^[^@\s]+@[^@\s]+\.[^@\s]+$";
        var validUsers = users
            .Where(u => !string.IsNullOrEmpty(u.Email) && Regex.IsMatch(u.Email, emailPattern));
        foreach (var user in validUsers)
        {
            Console.WriteLine($"{user.Name}: {user.Email}");
        }
    }
}
Alice: alice@example.com
Charlie: charlie@example.com

この例では、正規表現を使ってメールアドレスの形式を検証し、不正なメールアドレスを持つユーザーを除外しています。

Whereで条件を指定し、IsMatchでパターンマッチングを行っています。

これらの実務例は、LINQの強力なクエリ機能を活用して複雑なデータ操作を簡潔に実装する方法を示しています。

ログ分析やランキング生成、不正データのフィルタリングは多くの業務で頻繁に発生するため、LINQを使いこなすことで効率的かつ保守性の高いコードを書くことが可能です。

よくある落とし穴

LINQは便利な機能ですが、使い方を誤るとパフォーマンス低下やバグの原因になることがあります。

ここでは特に注意したい「Select先での副作用」「GroupByのメモリ肥大」「データベース側のN+1問題」について詳しく解説いたします。

Select先での副作用

LINQのSelectは本来、要素の変換や投影を行う純粋な関数として使うべきですが、処理の中で副作用(状態変更や外部への影響)を起こすコードを入れてしまうケースがあります。

これは予期せぬ動作やバグの原因となるため注意が必要です。

例えば、以下のようにSelect内で変数の値を変更するコードは副作用を伴います。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int counter = 0;
        var numbers = Enumerable.Range(1, 5);
        var query = numbers.Select(n =>
        {
            counter++;  // 副作用:外部変数の変更
            return n * 2;
        });
        // クエリは遅延実行なので、列挙時に副作用が発生
        foreach (var item in query)
        {
            Console.WriteLine(item);
        }
        Console.WriteLine($"処理回数: {counter}");
    }
}
2
4
6
8
10
処理回数: 5

この例ではcounterSelectの中で増加していますが、クエリが複数回列挙されると副作用も複数回発生し、予期しない結果になる恐れがあります。

副作用を避けるためには、Select内では純粋な変換処理だけを行い、状態変更や外部への影響は別の場所で行うことが望ましいです。

GroupByのメモリ肥大

GroupByは要素をキーでグループ化する便利な演算子ですが、大量データに対して使うとメモリ使用量が急増することがあります。

これはGroupByが内部で全要素を一時的にメモリに保持し、グループごとにまとめるためです。

例えば、数百万件のデータをGroupByでグループ化すると、メモリ不足やパフォーマンス低下を招くことがあります。

// 大量データの例(イメージ)
var largeData = Enumerable.Range(1, 10_000_000);
var grouped = largeData.GroupBy(n => n % 10);
// groupedの列挙時に大量のメモリを消費

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

  • 必要なデータだけを事前に絞り込む

Whereで条件を絞り、グループ化する要素数を減らす。

  • ストリーム処理やバッチ処理を検討する

一度に全件をグループ化せず、分割して処理します。

  • データベース側でグループ化を行う

LINQ to Entitiesなどを使い、SQLのGROUP BYで集計してから取得します。

  • メモリ使用量を監視し、必要に応じて環境を調整する

データベース側のN+1問題

N+1問題は、ORMやLINQ to Entitiesなどでよく発生するパフォーマンス問題です。

親テーブルのN件のデータを取得した後、関連する子テーブルのデータを1件ずつ個別に問い合わせるため、合計でN+1回のSQLが発行されてしまいます。

例えば、以下のようなコードはN+1問題を引き起こします。

using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
class Program
{
    static void Main()
    {
        using var context = new SampleContext();
        var orders = context.Orders.ToList(); // 親テーブルを取得
        foreach (var order in orders)
        {
            // 子テーブルのデータを遅延ロードで1件ずつ取得 → N回のSQL発行
            Console.WriteLine($"Order {order.Id} has {order.OrderItems.Count} items");
        }
    }
}

この場合、Ordersの取得で1回、OrderItemsの取得でN回のSQLが発行され、合計N+1回の問い合わせが発生します。

対策としては、Eager Loadingを使い、関連データを一度のクエリでまとめて取得します。

Entity FrameworkではIncludeメソッドを使います。

var ordersWithItems = context.Orders
    .Include(o => o.OrderItems)
    .ToList();
foreach (var order in ordersWithItems)
{
    Console.WriteLine($"Order {order.Id} has {order.OrderItems.Count} items");
}

これにより、1回のSQLで親子テーブルのデータを取得でき、N+1問題を回避できます。

これらの落とし穴はLINQを使う上でよく遭遇する問題です。

副作用のある処理を避け、メモリ使用量に注意し、データベースアクセスの効率化を図ることで、安定かつ高速なアプリケーションを実現できます。

バージョン別の注意点

C#とLINQはバージョンアップを重ねるごとに機能が拡充され、より表現力豊かで安全なコードが書けるようになっています。

ここではC# 3.0から11.0までの主な進化と、近年重要視されているnullableコンテキストへの適応について解説します。

C# 3.0から11.0までの進化

C# 3.0(2007年)

  • LINQの登場

C# 3.0でLINQが初めて導入されました。

  • クエリ構文とメソッド構文の両方が利用可能に
  • 匿名型、拡張メソッド、ラムダ式、式ツリーなどLINQを支える基盤機能が追加
  • IEnumerable<T>を対象にしたLINQ to Objectsが主流

C# 4.0(2010年)

  • 動的型付けの導入

dynamic型が追加され、LINQの柔軟性が向上。

  • 名前付き引数、オプション引数

メソッド呼び出しの可読性が向上。

C# 5.0(2012年)

  • 非同期プログラミングの強化

async/awaitキーワードが導入され、非同期LINQパターンの基礎が整備。

  • LINQクエリの非同期実行がより自然に

C# 6.0(2015年)

  • null条件演算子?.の追加

LINQでのnull安全なクエリ記述が簡潔に。

  • 文字列補間

クエリ結果の表示やログ出力が楽に。

C# 7.0(2017年)

  • タプルの強化

LINQの投影で複数値を返す際に便利。

  • パターンマッチングの導入

switch式やisパターンで条件分岐が強化。

C# 8.0(2019年)

  • 非同期ストリームIAsyncEnumerable<T>の追加

await foreachで非同期LINQが可能に。

  • nullable参照型の導入(オプション)

null安全性の向上。

  • インデックスと範囲演算子

配列やリストの部分取得が簡単に。

C# 9.0(2020年)

  • レコード型の追加

不変データ構造の表現が容易に。

  • トップレベルステートメント

簡潔なプログラム記述が可能です。

C# 10.0(2021年)

  • グローバルusingディレクティブ

名前空間の冗長な記述を削減。

  • 構造体の改良

パフォーマンス向上。

C# 11.0(2022年)

  • リストパターン

LINQのパターンマッチングでリストの条件判定が強化。

  • raw文字列リテラル

複雑な文字列(SQLやJSONなど)を簡単に記述可能です。

nullableコンテキストへの適応

C# 8.0で導入されたnullable参照型は、参照型の変数がnullを許容するかどうかをコンパイル時に明示的に区別し、null参照例外の発生を減らすための機能です。

LINQを使う際もこの機能に適応することが重要です。

nullableコンテキストの有効化

プロジェクトの.csprojファイルに以下を追加することで有効化します。

<Nullable>enable</Nullable>

これにより、参照型はデフォルトでnull非許容となり、nullを代入すると警告が出ます。

nullを許容したい場合はstring?のように?を付けます。

LINQクエリでの影響

nullableコンテキストが有効な場合、LINQのクエリ内でnullチェックを厳密に行う必要があります。

例えば、以下のようにnull許容型を扱う場合は、null条件演算子やnullチェックを適切に入れます。

string?[] names = { "Alice", null, "Bob" };
var nonNullNames = names
    .Where(name => !string.IsNullOrEmpty(name))
    .Select(name => name!.ToUpper()); // null許容を解除
foreach (var name in nonNullNames)
{
    Console.WriteLine(name);
}

name!はnull許容を解除する演算子で、コンパイラに「ここはnullでない」と伝えます。

ただし、実際にnullが入る可能性がある場合はWhereでしっかり除外することが必須です。

メソッドシグネチャの変更

拡張メソッドやカスタムメソッドの引数や戻り値もnullable対応を行う必要があります。

例えば、

public static IEnumerable<string?> FilterNullable(this IEnumerable<string?> source)
{
    return source.Where(s => s != null);
}

のように、引数や戻り値に?を付けてnull許容を明示します。

既存コードの移行

既存のコードをnullableコンテキストに対応させる際は、以下の点に注意します。

  • nullチェックを追加し、警告を解消します
  • !演算子でnull許容を解除する箇所を最小限に抑えます
  • 可能な限りnull非許容型を使い、null安全な設計を心がける

C#のバージョンアップに伴い、LINQの書き方や安全性も進化しています。

特にnullableコンテキストはコードの品質向上に寄与するため、最新の言語機能を理解し適切に対応することが重要です。

便利な補助API

LINQの標準機能は非常に強力ですが、より高度な操作や非同期処理を簡単に実現するための補助的なライブラリも存在します。

ここでは、拡張LINQライブラリとして有名なMoreLINQの主要機能と、非同期LINQをサポートするSystem.Linq.Asyncについて詳しく解説いたします。

MoreLINQの主要機能

MoreLINQは、標準LINQにない便利な拡張メソッドを多数提供するオープンソースライブラリです。

複雑なシーケンス操作やパターンを簡潔に記述できるため、実務でのデータ処理を効率化します。

代表的な機能をいくつか紹介します。

Batch

シーケンスを指定したサイズのチャンク(バッチ)に分割します。

大量データを分割して処理したい場合に便利です。

using System;
using System.Linq;
using MoreLinq;
class Program
{
    static void Main()
    {
        var numbers = Enumerable.Range(1, 10);
        var batches = numbers.Batch(3);
        foreach (var batch in batches)
        {
            Console.WriteLine($"Batch: {string.Join(", ", batch)}");
        }
    }
}
Batch: 1, 2, 3
Batch: 4, 5, 6
Batch: 7, 8, 9
Batch: 10

DistinctBy

特定のキーで重複を排除します。

標準LINQのDistinctは要素全体で比較しますが、DistinctByはキー指定で重複判定が可能です。

using System;
using System.Linq;
using MoreLinq;
class Program
{
    class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }
    static void Main()
    {
        var people = new[]
        {
            new Person { Name = "Alice", Age = 30 },
            new Person { Name = "Bob", Age = 25 },
            new Person { Name = "Alice", Age = 35 }
        };
        var distinctByName = people.DistinctBy(p => p.Name);
        foreach (var person in distinctByName)
        {
            Console.WriteLine($"{person.Name} ({person.Age})");
        }
    }
}
Alice (30)
Bob (25)

Window

スライディングウィンドウを作成し、連続した要素のグループを生成します。

時系列データの分析などに役立ちます。

using System;
using System.Linq;
using MoreLinq;
class Program
{
    static void Main()
    {
        var numbers = Enumerable.Range(1, 5);
        var windows = numbers.Window(3);
        foreach (var window in windows)
        {
            Console.WriteLine($"Window: {string.Join(", ", window)}");
        }
    }
}
Window: 1, 2, 3
Window: 2, 3, 4
Window: 3, 4, 5

MaxBy / MinBy

指定したキーで最大値・最小値の要素を取得します。

C# 9以降で標準化されましたが、MoreLINQでは以前から利用可能でした。

var oldest = people.MaxBy(p => p.Age);
Console.WriteLine($"Oldest: {oldest.Name} ({oldest.Age})");

MoreLINQは他にも多数の便利なメソッドを提供しており、NuGetから簡単に導入できます。

標準LINQの不足を補い、複雑なデータ操作をシンプルに記述したい場合におすすめです。

System.Linq.Asyncで非同期拡張

System.Linq.Asyncは、IAsyncEnumerable<T>に対するLINQ拡張メソッドを提供するライブラリで、非同期ストリームの操作を簡単に行えます。

await foreachと組み合わせて使うことで、非同期データのフィルタリングや変換、集計が可能です。

代表的なメソッド

  • ToListAsync()

非同期にシーケンスをリストに変換します。

  • Where()

非同期シーケンスの要素を条件でフィルタリングします。

  • Select()

非同期シーケンスの要素を変換します。

  • FirstAsync(), SingleAsync(), AnyAsync()など

非同期で要素の取得や存在判定を行います。

使用例

using System;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq.Async;
class Program
{
    static async Task Main()
    {
        var asyncNumbers = GetNumbersAsync();
        var evenNumbers = asyncNumbers.Where(n => n % 2 == 0);
        var list = await evenNumbers.ToListAsync();
        foreach (var n in list)
        {
            Console.WriteLine(n);
        }
    }
    static async IAsyncEnumerable<int> GetNumbersAsync()
    {
        for (int i = 1; i <= 10; i++)
        {
            await Task.Delay(100); // 模擬的な非同期処理
            yield return i;
        }
    }
}
2
4
6
8
10

ポイント

  • System.Linq.Asyncは.NET標準の非同期ストリームIAsyncEnumerable<T>に対応したLINQ演算子を提供します
  • 非同期I/Oやストリーム処理での待機時間を効率的に扱えます
  • NuGetパッケージSystem.Linq.Asyncをインストールして利用します

これらの補助APIを活用することで、標準LINQの機能を拡張し、より高度で効率的なデータ操作や非同期処理が可能になります。

特に大量データや非同期I/Oを扱う現代のアプリケーション開発においては、MoreLINQSystem.Linq.Asyncは強力な武器となります。

LINQと設計パターン

LINQはデータ操作を簡潔に記述できる強力なツールですが、設計パターンと組み合わせることで、より保守性や拡張性の高いシステムを構築できます。

ここでは、リポジトリパターンとの相性と、クリーンアーキテクチャにおけるクエリ分離について詳しく解説いたします。

リポジトリパターンとの相性

リポジトリパターンは、データアクセスの抽象化を目的とした設計パターンで、ドメイン層からデータソースへのアクセスを分離します。

LINQはこのパターンと非常に相性が良く、リポジトリの実装でクエリを簡潔に記述できるため、コードの可読性と保守性が向上します。

リポジトリパターンの基本構造

  • インターフェース

データ操作の契約を定義し、ドメイン層はこれに依存します。

  • 実装クラス

実際のデータアクセス(LINQ to EntitiesやLINQ to Objectsなど)を行います。

LINQを使ったリポジトリの例

using System;
using System.Collections.Generic;
using System.Linq;
public interface IProductRepository
{
    IEnumerable<Product> GetAll();
    IEnumerable<Product> GetByCategory(string category);
    Product GetById(int id);
}
public class ProductRepository : IProductRepository
{
    private readonly List<Product> _products;
    public ProductRepository()
    {
        // サンプルデータ
        _products = new List<Product>
        {
            new Product { Id = 1, Name = "商品A", Category = "食品", Price = 100 },
            new Product { Id = 2, Name = "商品B", Category = "家電", Price = 2000 },
            new Product { Id = 3, Name = "商品C", Category = "食品", Price = 150 }
        };
    }
    public IEnumerable<Product> GetAll()
    {
        return _products.AsEnumerable();
    }
    public IEnumerable<Product> GetByCategory(string category)
    {
        return _products.Where(p => p.Category == category);
    }
    public Product GetById(int id)
    {
        return _products.FirstOrDefault(p => p.Id == id);
    }
}
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Category { get; set; }
    public decimal Price { get; set; }
}
class Program
{
    static void Main()
    {
        IProductRepository repository = new ProductRepository();
        var foods = repository.GetByCategory("食品");
        foreach (var product in foods)
        {
            Console.WriteLine($"{product.Name} - {product.Price}円");
        }
    }
}
商品A - 100円
商品C - 150円

メリット

  • データアクセスの一元管理

LINQクエリをリポジトリ内に集約し、変更に強い設計にできます。

  • テスト容易性の向上

インターフェースを使うことでモック実装が可能になり、単体テストがしやすい。

  • ドメイン層の関心分離

ドメインロジックはデータアクセスの詳細を意識せずに済みます。

注意点

  • リポジトリのメソッドが増えすぎると肥大化するため、必要なクエリを適切に整理することが重要でしょう
  • LINQの遅延実行により、呼び出し側での列挙タイミングに注意が必要でしょう

クリーンアーキテクチャでのクエリ分離

クリーンアーキテクチャは、システムを層に分割し依存関係を内側に向けることで、保守性や拡張性を高める設計思想です。

LINQを使う際も、クエリ処理を適切な層に分離することが推奨されます。

クリーンアーキテクチャの層構成例

  • エンティティ層(ドメインモデル)

ビジネスルールやドメインオブジェクトを定義。

  • ユースケース層(アプリケーションサービス)

ビジネスロジックの実装やユースケースの調整。

  • インフラ層

データベースアクセスや外部サービスとの連携。

  • プレゼンテーション層

UIやAPIの実装。

クエリ分離のポイント

  • クエリはインフラ層で実装

LINQ to EntitiesやDapperなどの具体的なデータアクセスはインフラ層に置きます。

  • ユースケース層はインターフェースを通じてクエリを呼び出す

ユースケース層はリポジトリやクエリサービスのインターフェースに依存し、実装詳細を知らない。

  • クエリ専用のサービスを作成することもある

複雑な読み取り専用クエリは、リポジトリとは別にクエリサービスとして分離し、読み取り専用の最適化を行います。

例:クエリサービスのインターフェース

public interface IProductQueryService
{
    IEnumerable<ProductDto> GetProductsByCategory(string category);
}

例:ユースケース層での利用

public class ProductUseCase
{
    private readonly IProductQueryService _queryService;
    public ProductUseCase(IProductQueryService queryService)
    {
        _queryService = queryService;
    }
    public void DisplayProducts(string category)
    {
        var products = _queryService.GetProductsByCategory(category);
        foreach (var p in products)
        {
            Console.WriteLine($"{p.Name} - {p.Price}円");
        }
    }
}

メリット

  • 関心の分離

読み取り専用クエリと書き込み処理を分けることで、最適化や保守がしやすくなります。

  • テストの容易さ

クエリサービスをモック化しやすく、ユースケースのテストが簡単。

  • 柔軟なデータアクセス戦略

クエリ専用の最適化やキャッシュ導入がしやすい。

LINQはリポジトリパターンやクリーンアーキテクチャと組み合わせることで、堅牢で拡張性の高いシステム設計に貢献します。

設計パターンの原則を守りつつ、LINQの強力な表現力を活かすことが重要です。

まとめ

本記事では、C#のLINQを基礎から応用まで幅広く解説しました。

LINQの基本構文や主要データソース別の使い方、基本操作やコントロールフロー演算子、生成・変換演算子の活用法を理解できます。

さらに、パフォーマンス最適化やエラー処理、カスタム拡張メソッド、非同期パターン、実務での具体例、よくある落とし穴、バージョン別の注意点、補助API、設計パターンとの連携まで網羅。

LINQの強力な機能を適切に使いこなすことで、効率的で保守性の高いC#開発が可能になります。

関連記事

Back to top button
目次へ