bucket-sort logo bucket-sort

プログラミングとインフラエンジニアリングの覚え書き

  • Posts
  • About
  • Contact
  1. Home
  2. All Posts
  3. [C#] LINQ to Objects を理解する: クエリの内部表現

[C#] LINQ to Objects を理解する: クエリの内部表現

Jun 22, 2026 C# , .NET bucket-sort

前回は、LINQ to Objects のページング、集合演算、集計を見ました。

今回は、LINQ クエリが内部的にどのように表現されるのかを整理します。

LINQ のクエリ構文は、専用の魔法のように見えるかもしれません。

しかし、LINQ to Objects では、多くの場合、最終的に Enumerable クラスの拡張メソッド呼び出しとして扱われます。

クエリ構文はメソッド構文に変換される

まず、次のクエリ構文を見てみます。

int[] numbers = { 1, 2, 3, 4, 5, 6 };

var query =
    from number in numbers
    where number % 2 == 0
    select number * 10;

これは、概念的には次のメソッド構文に変換されます。

var query = numbers
    .Where(number => number % 2 == 0)
    .Select(number => number * 10);

さらに、拡張メソッドを通常の静的メソッド呼び出しとして書くと、次のようになります。

var query = Enumerable.Select(
    Enumerable.Where(numbers, number => number % 2 == 0),
    number => number * 10
);

つまり、クエリ構文は読みやすい表記であり、裏側では Where や Select の呼び出しに対応しています。

Enumerable クラス

LINQ to Objects の多くのメソッドは、System.Linq.Enumerable クラスに定義されています。

using System.Linq;

Enumerable.Where のイメージは、次のようなメソッドです。

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

第 1 引数の source が対象のコレクションです。

第 2 引数の predicate が、要素を残すかどうかを判定する処理です。

this IEnumerable<TSource> source という形になっているため、拡張メソッドとして呼び出せます。

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

通常の静的メソッドとして呼ぶなら、次のようになります。

Enumerable.Where(numbers, number => number % 2 == 0)

ラムダ式を使ったクエリ

LINQ のメソッド構文では、ラムダ式がよく使われます。

var result = numbers
    .Where(number => number % 2 == 0)
    .Select(number => number * 10);

Where に渡しているラムダ式は、次の形です。

number => number % 2 == 0

これは、int を受け取り、bool を返す処理です。

つまり、Func<int, bool> として扱えます。

Func<int, bool> isEven = number => number % 2 == 0;

Select に渡しているラムダ式は、次の形です。

number => number * 10

これは、int を受け取り、int を返す処理です。

Func<int, int> multiplyByTen = number => number * 10;

このように、LINQ の条件や変換は、デリゲートとして渡されています。

ラムダ式を変数に分ける

ラムダ式を変数に分けると、LINQ が何を受け取っているのかが見えやすくなります。

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3, 4, 5, 6 };

        Func<int, bool> isEven = number => number % 2 == 0;
        Func<int, int> multiplyByTen = number => number * 10;

        IEnumerable<int> result = numbers
            .Where(isEven)
            .Select(multiplyByTen);

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

実行結果です。

20
40
60

Where は判定処理を受け取り、Select は変換処理を受け取っています。

LINQ は、処理そのものを引数として渡すことで柔軟な問い合わせを実現しています。

匿名メソッドで書く

ラムダ式の代わりに、匿名メソッドでも書けます。

var result = numbers
    .Where(delegate (int number)
    {
        return number % 2 == 0;
    })
    .Select(delegate (int number)
    {
        return number * 10;
    });

実行結果はラムダ式と同じです。

ラムダ式は、匿名メソッドをより簡潔に書ける構文だと考えられます。

LINQ では条件や変換を頻繁に書くため、ラムダ式の方が読みやすくなります。

生のデリゲートで書く

さらに古い書き方として、デリゲートオブジェクトを明示的に作ることもできます。

Func<int, bool> isEven = new Func<int, bool>(IsEven);
Func<int, int> multiplyByTen = new Func<int, int>(MultiplyByTen);

var result = numbers
    .Where(isEven)
    .Select(multiplyByTen);
static bool IsEven(int number)
{
    return number % 2 == 0;
}

static int MultiplyByTen(int number)
{
    return number * 10;
}

現在は、次のようにラムダ式で書くのが一般的です。

var result = numbers
    .Where(number => number % 2 == 0)
    .Select(number => number * 10);

しかし、内部的には「条件や変換の処理をデリゲートとして渡している」という点は変わりません。

クエリ構文とメソッド構文の対応

代表的なクエリ構文とメソッド構文の対応を見てみます。

var query =
    from product in products
    where product.InStock
    orderby product.Price
    select product.Name;

これは、概念的には次のように対応します。

var query = products
    .Where(product => product.InStock)
    .OrderBy(product => product.Price)
    .Select(product => product.Name);
クエリ構文 メソッド構文
where Where
orderby OrderBy / OrderByDescending
select Select
複数の from SelectMany
group ... by ... GroupBy
join Join

すべてを完全に一対一で暗記する必要はありません。

まずは、クエリ構文はメソッド呼び出しに変換される、という感覚を持つことが大切です。

複数の from と SelectMany

クエリ構文で複数の from を使うと、入れ子のコレクションを平らに扱えます。

var query =
    from order in orders
    from item in order.Items
    select item;

これは、概念的には SelectMany に対応します。

var query = orders.SelectMany(order => order.Items);

SelectMany は、コレクションの中にあるコレクションを 1 つの流れにします。

List<Order> orders = new List<Order>
{
    new Order(1, new List<string> { "ノート", "ペン" }),
    new Order(2, new List<string> { "マウス" })
};

var items =
    from order in orders
    from item in order.Items
    select item;
foreach (string item in items)
{
    Console.WriteLine(item);
}

実行結果です。

ノート
ペン
マウス

GroupBy の内部イメージ

GroupBy は、キーごとに要素をまとめます。

var groups = products.GroupBy(product => product.Category);

クエリ構文では、次のように書けます。

var groups =
    from product in products
    group product by product.Category;

GroupBy の結果は、キーと要素の集まりです。

foreach (var group in groups)
{
    Console.WriteLine(group.Key);

    foreach (Product product in group)
    {
        Console.WriteLine($"  {product.Name}");
    }
}

group.Key にはカテゴリ名が入り、group 自体はそのカテゴリに属する商品を列挙できます。

遅延実行は Enumerable の実装による

LINQ to Objects の多くのメソッドは、IEnumerable<T> を返します。

IEnumerable<int> query = numbers.Where(number => number % 2 == 0);

IEnumerable<T> は、要素を順番に取り出すためのインターフェースです。

そのため、Where を呼んだ時点では、結果のリストが作られているとは限りません。

実際に foreach などで列挙されたときに、必要な要素を順番に取り出します。

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

この遅延実行により、LINQ は複数の処理を効率よくつなげられます。

var query = numbers
    .Where(number => number % 2 == 0)
    .Select(number => number * 10)
    .Take(2);

このような処理では、必要な分だけ要素を処理しながら結果を返せます。

即時実行メソッド

一方で、すぐに結果を確定するメソッドもあります。

List<int> list = query.ToList();
int count = query.Count();
int sum = query.Sum();
bool exists = query.Any();

これらは、結果を作るためにクエリを列挙します。

種類 例
列挙を返す Where, Select, OrderBy, Take, Skip
結果を確定する ToList, ToArray, Count, Sum, Any, First

LINQ を読むときは、「この時点で実行されるのか」「あとで列挙されたときに実行されるのか」を意識すると、挙動を理解しやすくなります。

実践例: クエリを組み立ててから実行する

遅延実行を利用すると、条件に応じてクエリを組み立ててから、最後に実行できます。

using System;
using System.Collections.Generic;
using System.Linq;

class Product
{
    public string Name { get; }
    public string Category { get; }
    public int Price { get; }
    public bool InStock { get; }

    public Product(string name, string category, int price, bool inStock)
    {
        Name = name;
        Category = category;
        Price = price;
        InStock = inStock;
    }
}

class Program
{
    static void Main()
    {
        List<Product> products = new List<Product>
        {
            new Product("ノート", "文房具", 200, true),
            new Product("ペン", "文房具", 150, false),
            new Product("キーボード", "PC", 4500, true),
            new Product("マウス", "PC", 2500, true),
            new Product("モニター", "PC", 24000, false)
        };

        string? category = "PC";
        int? maxPrice = 5000;
        bool onlyInStock = true;

        IEnumerable<Product> query = products;

        if (!string.IsNullOrEmpty(category))
        {
            query = query.Where(product => product.Category == category);
        }

        if (maxPrice is not null)
        {
            query = query.Where(product => product.Price <= maxPrice.Value);
        }

        if (onlyInStock)
        {
            query = query.Where(product => product.InStock);
        }

        List<Product> result = query
            .OrderBy(product => product.Price)
            .ToList();

        foreach (Product product in result)
        {
            Console.WriteLine($"{product.Name}: {product.Price}円");
        }
    }
}

実行結果です。

マウス: 2500円
キーボード: 4500円

query に条件を少しずつ追加し、最後に ToList() で実行しています。

検索画面や API の絞り込み処理でよく使う形です。

まとめ

LINQ to Objects のクエリは、内部的には Enumerable の拡張メソッド呼び出しとして考えられます。

  • クエリ構文は、メソッド構文に変換される
  • Where、Select、OrderBy などは Enumerable の拡張メソッドである
  • LINQ の条件や変換は、ラムダ式として渡される
  • ラムダ式は Func<T, TResult> などのデリゲートとして扱われる
  • ラムダ式の代わりに匿名メソッドや明示的なデリゲートでも書ける
  • 複数の from は SelectMany に対応する
  • group ... by ... は GroupBy に対応する
  • 多くの LINQ メソッドは遅延実行される
  • ToList、Count、Sum、Any などは結果を確定する

LINQ は便利な構文ですが、根底には IEnumerable<T>、拡張メソッド、ラムダ式、デリゲートがあります。

これまで見てきた C# の機能がつながると、LINQ は単なる便利メソッドの集まりではなく、C# らしいデータ処理の書き方として理解できるようになります。

C# .NET LINQ LINQ to Objects Enumerable ラムダ式 デリゲート
← [C#] LINQ to Objects を理解する: ページング・集合演算・集計 [C#] Windows のプロセスを理解する: Process / Thread / ProcessStartInfo 徹底入門 →

Related Posts

  • [C#] LINQ to Objects を理解する: ページング・集合演算・集計 Jun 21, 2026
  • [C#] LINQ to Objects を理解する: フィルター・射影・並べ替え Jun 20, 2026
  • [C#] LINQ to Objects を理解する: 基本構文と遅延実行 Jun 19, 2026
  • [C#] ラムダ式を理解する Jun 18, 2026

Table of Contents

  • クエリ構文はメソッド構文に変換される
  • Enumerable クラス
  • ラムダ式を使ったクエリ
  • ラムダ式を変数に分ける
  • 匿名メソッドで書く
  • 生のデリゲートで書く
  • クエリ構文とメソッド構文の対応
  • 複数の from と SelectMany
  • GroupBy の内部イメージ
  • 遅延実行は Enumerable の実装による
  • 即時実行メソッド
  • 実践例: クエリを組み立ててから実行する
  • まとめ

Recent Posts

  • [C#] プラグインで拡張できるアプリケーションを設計する Jul 9, 2026
  • [C#] 属性でコードに意味を与える Jul 8, 2026
  • [C#] 外部アセンブリを読み込み実行時に型を呼び出す Jul 7, 2026
  • [C#] リフレクションで型の中身を調べる Jul 6, 2026
  • [C#] 型メタデータとリフレクションの基礎を理解する Jul 5, 2026

Categories

  • C#120
  • .NET119
  • AWS27
  • Laravel16
  • Linux15
  • MySQL9
  • Apache8
  • PHP8
  • DynamoDB6
  • セキュリティ6
  • Nginx5
  • WordPress4
  • インフラ4
  • Hugo3
  • .NET Framework1
  • Aurora1
  • Diagnostics1
  • Filament1
  • Git1
  • SQS1

Tags

  • C#
  • .NET
  • AWS
  • Laravel
  • コレクション
  • PHP
  • セキュリティ
  • MySQL
  • Linux
  • パフォーマンス
  • Apache
  • System.Collections.Generic
  • デリゲート
  • Code Snippet
  • DynamoDB
  • LINQ
  • NoSQL
  • PHP-FPM
  • RDS
  • System.Collections
  • Windows
  • メモリ管理
  • リフレクション
  • DoS
  • Nginx
  • WordPress
  • メタデータ
  • ラムダ式
  • 監視
  • 設計
  • Amazon Linux 2023
  • Delegate
  • Docker
  • IDisposable
  • Ipset
  • Iptables
  • LINQ to Objects
  • OPCache
  • Pointer
  • Reflection
  • System.Collections.Specialized
  • Unsafe
  • Webサーバー
  • インターフェース
  • オブジェクト指向
  • クラス設計
  • デザインパターン
  • パターンマッチング
  • ポインター
  • 継承
Powered by Hugo & Explore Theme.