2026年版 C# LINQ上級ガイド:演算子、パフォーマンス最適化、面接対策

LINQ の上級演算子、遅延実行、パフォーマンス最適化を詳しく解説します。GroupBy、SelectMany、クエリ最適化、および.NET開発者向け面接質問を網羅しています。

C# LINQの上級演算子とパフォーマンス最適化

C# LINQ の上級演算子は、経験豊富な.NET開発者と初心者を区別する重要なスキルです。WhereSelect で基本的なフィルタリングは処理できますが、実際のコードベースでは GroupBySelectMany、遅延実行パターン、およびパフォーマンスを意識したクエリ構成の習熟が求められます。

面接での重要ポイント

面接官は候補者に遅延実行と即時実行の違いを説明させたり、遅いLINQクエリを最適化させたりすることがよくあります。内部で何が起こっているかを理解することが、シニア候補者と構文を暗記しているだけの候補者を区別します。

LINQにおける遅延実行の理解

LINQクエリは宣言時には実行されません。実行は結果が列挙されるとき、通常は foreachToList()、または ToArray() を介して行われます。この動作は遅延実行と呼ばれ、データベースやコレクションの繰り返しスキャンなしにクエリを構成することを可能にします。

DeferredExecutionDemo.cscsharp
var numbers = new List<int> { 1, 2, 3, 4, 5 };

// Query is defined but not executed
var query = numbers.Where(n => n > 2);

// Adding an element after query definition
numbers.Add(6);

// Execution happens here, includes 6
foreach (var n in query)
{
    Console.WriteLine(n); // Output: 3, 4, 5, 6
}

クエリは numbers の内容ではなく参照をキャプチャします。foreach が反復処理するとき、更新されたコレクションが参照されます。このパターンは動的にクエリを段階的に構築する場合に有用ですが、開発者がスナップショット動作を期待している場合にはバグの原因となります。

即時実行を強制するには、ToList()ToArray()、または ToDictionary() を呼び出します。これらのメソッドはソースを一度列挙し、結果をキャッシュします。

GroupBy:基本的なグループ化を超えて

GroupBy はフラットなコレクションを階層構造に変換します。この演算子は最大4つのパラメータを受け入れます:キーセレクター、要素セレクター、結果セレクター、およびカスタムコンパレラーです。

OrderAnalytics.cscsharp
public record Order(int Id, string Customer, decimal Amount, DateTime Date);

var orders = GetOrders();

// Group by customer with monthly breakdown
var customerMonthlyTotals = orders
    .GroupBy(
        o => new { o.Customer, Month = new DateTime(o.Date.Year, o.Date.Month, 1) },
        (key, group) => new
        {
            key.Customer,
            key.Month,
            Total = group.Sum(o => o.Amount),
            OrderCount = group.Count()
        })
    .OrderBy(x => x.Customer)
    .ThenBy(x => x.Month);

// Output: Customer "Acme" spent $12,500 across 8 orders in August 2026

複合キーは顧客と月の両方でグループ化します。結果セレクターは分析に適した形式に直接プロジェクションするため、2回目の Select パスを回避できます。

メモリに関する考慮事項

GroupBy はグループを形成するためにソース全体をバッファリングする必要があります。大規模なデータセットやストリーミングシナリオでは、EF Core を使用したデータベース側でのグループ化を検討するか、グループ化された結果に複数回アクセスする場合は ToLookup を使用してください。

SelectMany:ネストされたコレクションのフラット化

SelectMany は一対多の関係を単一のシーケンスにフラット化します。この演算子は各要素が0個以上の結果を生成するシナリオを処理します。

ProductCatalog.cscsharp
public record Category(string Name, List<Product> Products);
public record Product(string Name, decimal Price);

var categories = GetCategories();

// Flatten all products with their category names
var allProducts = categories
    .SelectMany(
        category => category.Products,
        (category, product) => new
        {
            CategoryName = category.Name,
            ProductName = product.Name,
            product.Price
        });

// Filter after flattening
var expensiveProducts = allProducts
    .Where(p => p.Price > 100)
    .OrderByDescending(p => p.Price);

SelectMany の2パラメータオーバーロードは親要素と子要素の両方へのアクセスを提供し、両方のレベルからデータを組み合わせたプロジェクションを可能にします。このパターンはネストされた foreach ループを宣言的なアプローチに置き換えます。

クエリ構文は、複雑な結合に対してより読みやすいと感じる開発者もいる代替手段を提供します:

QuerySyntaxFlatten.cscsharp
var productsWithCategories =
    from category in categories
    from product in category.Products
    where product.Price > 100
    select new { category.Name, product };

両方とも同じ SelectMany 呼び出しにコンパイルされます。チームの規約とクエリの複雑さに基づいて選択してください。

LINQパフォーマンスの最適化

LINQ抽象化にはオーバーヘッドが伴います。チェーンされた各演算子はイテレータオブジェクトを作成し、ラムダ式はデリゲート呼び出しコストを追加します。毎秒数千の要素を処理するホットパスでは、これらのコストが蓄積されます。

複数回の列挙を避ける

MultipleEnumerationProblem.cscsharp
IEnumerable<Order> GetExpensiveOrders(IEnumerable<Order> orders)
{
    var filtered = orders.Where(o => o.Amount > 1000);
    
    // WARNING: Enumerates twice if orders is not materialized
    if (!filtered.Any())
        return Enumerable.Empty<Order>();
    
    return filtered.OrderBy(o => o.Date);
}

orders がデータベースクエリやファイルストリームから来る場合、このコードはソースを2回実行します。複数の操作の前に ToList() でマテリアライズしてください:

FixedMultipleEnumeration.cscsharp
IEnumerable<Order> GetExpensiveOrders(IEnumerable<Order> orders)
{
    var filtered = orders.Where(o => o.Amount > 1000).ToList();
    
    if (filtered.Count == 0)
        return Enumerable.Empty<Order>();
    
    return filtered.OrderBy(o => o.Date);
}

適切な演算子の選択

一部のLINQ演算子は特定のコレクション型に対して最適化された実装を持っています:

操作遅いバージョン速いバージョン備考
カウントcollection.Count()collection.CountICollection<T> にはプロパティを使用
要素アクセスcollection.ElementAt(5)collection[5]IList<T> にはインデクサーを使用
存在チェックcollection.Count() > 0collection.Any()Any() は短絡評価する
最初のマッチcollection.Where(x).First()collection.First(x)シングルパス vs 2つのイテレータ

.NETの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

Aggregate:カスタムリダクション操作

Aggregate はアキュムレータ関数を適用してシーケンスから単一の結果を構築します。SumMaxAverage は一般的なケースを処理しますが、Aggregate はカスタムリダクションを可能にします。

CustomAggregation.cscsharp
var transactions = new List<Transaction>
{
    new("Deposit", 1000),
    new("Withdrawal", -200),
    new("Deposit", 500),
    new("Fee", -25)
};

// Running balance with audit trail
var balanceHistory = transactions.Aggregate(
    new List<(string Op, decimal Balance)>(),
    (history, tx) =>
    {
        var previousBalance = history.Count > 0 ? history[^1].Balance : 0;
        history.Add((tx.Type, previousBalance + tx.Amount));
        return history;
    });

// Output: [(Deposit, 1000), (Withdrawal, 800), (Deposit, 1300), (Fee, 1275)]

3パラメータオーバーロードはシード値と最終結果セレクターを追加し、アキュムレータ型が結果型と異なる場合に有用です。

LINQ to Objects vs. LINQ to Entities

クエリがどこで実行されるかを理解することで、そのパフォーマンス特性が決まります。LINQ to Objects は C# デリゲートを使用してメモリ内で実行されます。LINQ to Entities(EF Core)は式を SQL に変換します。

EFCoreQueryOptimization.cscsharp
using var context = new AppDbContext();

// BAD: Loads all orders, filters in memory
var badQuery = context.Orders
    .ToList()
    .Where(o => o.Amount > 1000);

// GOOD: Filters in database, loads only matching rows
var goodQuery = context.Orders
    .Where(o => o.Amount > 1000)
    .ToList();

ToList() の配置がすべてを変えます。悪い例では、SQL Server は数千行を返し、その後 C# がそのほとんどを破棄します。良い例では、WHERE 句がサーバー側で実行されます。

複雑なプロジェクションの場合、EF Core 9(2026年8月現在)は、関連するコレクションを読み込む際のデカルト爆発を回避するための分割クエリをサポートしています。

よくあるLINQ面接質問

技術面接では、概念的な質問やライブコーディングを通じてLINQの理解を頻繁に確認します。以下のシナリオは.NET開発者の面接で定期的に出現します。

「IEnumerableとIQueryableの違いは何ですか?」

IEnumerable<T> はメモリ内のデリゲートで動作します。チェーンされた各演算子は要素を1つずつ処理するイテレータを作成します。ランタイムは演算子間で最適化できません。

IQueryable<T> は式ツリーで動作します。クエリ全体がデータ構造にコンパイルされ、プロバイダ(例えばEF Core)が SQL に変換します。これにより、サーバー側でのフィルタリング、ソート、プロジェクションが可能になります。

IQueryableDemo.cscsharp
// Expression tree, translates to SQL WHERE clause
IQueryable<Order> dbQuery = context.Orders.Where(o => o.Amount > 1000);

// Delegate, executes in C# memory
IEnumerable<Order> memoryQuery = orders.Where(o => o.Amount > 1000);

外部データソースをクエリする場合は IQueryable を選択してください。メモリ内コレクションの場合、またはクエリが正規表現やカスタムメソッドなどの C# 専用機能を使用する必要がある場合は IEnumerable を選択してください。

「コレクション内の重複を見つけるにはどうすればよいですか?」

このコーディング問題は GroupBy とフィルタリングをテストします:

FindDuplicates.cscsharp
var emails = new[] { "a@test.com", "b@test.com", "a@test.com", "c@test.com" };

var duplicates = emails
    .GroupBy(e => e, StringComparer.OrdinalIgnoreCase)
    .Where(g => g.Count() > 1)
    .Select(g => g.Key);

// Output: ["a@test.com"]

解決策は要素自体でグループ化し、1つ以上のメンバーを持つグループをフィルタリングし、キーをプロジェクションします。StringComparer.OrdinalIgnoreCase パラメータは大文字小文字を区別しないメールマッチングを処理します。

「LINQでページネーションをどのように実装しますか?」

Pagination.cscsharp
public IEnumerable<Product> GetProductsPage(int pageNumber, int pageSize)
{
    return context.Products
        .OrderBy(p => p.Id)
        .Skip((pageNumber - 1) * pageSize)
        .Take(pageSize)
        .ToList();
}

OrderBy 句は決定論的なページネーションに必須です。これがないと、データベースエンジンは任意の順序で行を返す可能性があり、アイテムが複数のページに表示されたり、完全に消えたりする原因となります。

より多くのC#面接質問については、SharpSkill でパターンマッチング、レコード、Null許容参照型などの高度な機能をカバーしています。

保守性の高いLINQクエリの記述

クエリの可読性は、チェーンが5つまたは6つの演算子を超えると急速に低下します。中間結果を名前付き変数に抽出するか、複雑なロジックを別のメソッドに分割してください。

MaintainableLINQ.cscsharp
public IEnumerable<CustomerReport> GenerateReports(IEnumerable<Order> orders)
{
    var ordersByCustomer = GroupOrdersByCustomer(orders);
    var reportsWithMetrics = CalculateCustomerMetrics(ordersByCustomer);
    return ApplyBusinessRules(reportsWithMetrics);
}

private IEnumerable<IGrouping<string, Order>> GroupOrdersByCustomer(
    IEnumerable<Order> orders)
{
    return orders
        .Where(o => o.Status == OrderStatus.Completed)
        .GroupBy(o => o.CustomerId);
}

private IEnumerable<CustomerReport> CalculateCustomerMetrics(
    IEnumerable<IGrouping<string, Order>> groups)
{
    return groups.Select(g => new CustomerReport
    {
        CustomerId = g.Key,
        TotalSpent = g.Sum(o => o.Amount),
        OrderCount = g.Count(),
        AverageOrderValue = g.Average(o => o.Amount)
    });
}

この構造により、各変換を分離してテストできます。メソッド名はインラインコメントよりも意図をよく文書化します。

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

.NET開発者向け重要ポイント

  • 遅延実行はクエリの評価を列挙まで遅延させます。スナップショット動作が必要な場合、または複数の列挙がデータソースに繰り返しヒットする場合は、ToList() または ToArray() を使用して結果をマテリアライズしてください。
  • GroupBy はソース全体をバッファリングします。大規模なデータセットでは、行をメモリに引き込むのではなく、EF Core でデータベースにグループ化をプッシュしてください。
  • SelectMany はネストされたコレクションをフラット化します。2パラメータオーバーロードは、組み合わせたプロジェクションのために親要素と子要素の両方へのアクセスを提供します。
  • IQueryable はプロバイダ変換用の式ツリーを構築します。IEnumerable はメモリ内でデリゲートを実行します。間違ったインターフェースを選択すると、フルテーブルスキャンが発生します。
  • 演算子の配置はEF Core パフォーマンスにとって重要です。WhereOrderByTakeToList() の前に配置して、フィルターをサーバー側で実行してください。
  • 面接質問では、遅延実行の理解、GroupBy を使用した重複検出、および IEnumerable vs. IQueryable の区別がよくテストされます。
今日のチャレンジ

.NET のバグを見つけられますか

実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

Anthony Fillion-Maillet

執筆

Anthony Fillion-Maillet

SharpSkill 創業者

10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。

2026年8月23日 更新

タグ

#C#
#LINQ
#.NET
#パフォーマンス
#面接

共有

関連記事