LINQ

【C#】LINQでスマートにデータ抽出!Where句と拡張メソッドで実現する多彩なフィルタリング術

LINQのフィルタリングは、Whereなどの拡張メソッドやSQL風クエリ構文で条件抽出し、遅延実行により不要な計算を避けながらデータを効率的に扱える手法です。

ラムダ式で複数条件も簡潔に書け、コレクション操作を読みやすく統一できます。

目次から探す
  1. LINQフィルタリングの基礎
  2. 単純条件での抽出
  3. 複数条件の組み合わせ
  4. インデックスを用いたフィルタリング
  5. Nullと例外への配慮
  6. 文字列特化のフィルタリング
  7. 日付・時刻データの扱い
  8. 複合オブジェクトの条件抽出
  9. コレクション種類別のアプローチ
  10. パフォーマンス最適化
  11. 条件式の動的生成
  12. 並列・非同期シナリオ
  13. よくある落とし穴と対策
  14. 実務サンプルケース
  15. まとめ

LINQフィルタリングの基礎

LINQ(Language Integrated Query)は、C#でデータの抽出や操作を簡潔に記述できる機能です。

特にフィルタリングにおいては、Where拡張メソッドやクエリ式のwhere句を使うことで、条件に合致する要素だけを効率よく抽出できます。

ここでは、LINQのフィルタリングの基本的な仕組みや特徴について詳しく解説いたします。

Where拡張メソッドの仕組み

Whereメソッドは、LINQの中でも最もよく使われる拡張メソッドの一つです。

これは、IEnumerable<T>型のコレクションに対して呼び出すことができ、指定した条件に合致する要素だけを抽出して新しいシーケンスを返します。

Whereメソッドのシグネチャは以下のようになっています。

public static IEnumerable<TSource> Where<TSource>(
    this IEnumerable<TSource> source,
    Func<TSource, bool> predicate
);
  • sourceはフィルタリング対象のコレクションです
  • predicateは各要素に対して真偽値を返す関数(ラムダ式など)で、trueを返した要素だけが結果に含まれます

例えば、整数の配列から偶数だけを抽出する場合は以下のように記述します。

int[] numbers = { 1, 2, 3, 4, 5, 6 };
var evenNumbers = numbers.Where(n => n % 2 == 0);
foreach (var num in evenNumbers)
{
    Console.WriteLine(num);
}

このコードでは、n => n % 2 == 0というラムダ式がpredicateとして渡され、偶数の要素だけがevenNumbersに含まれます。

Whereメソッドは元のコレクションを変更せず、新しいIEnumerable<T>を返すため、元のデータは安全に保たれます。

また、Whereは遅延実行を行うため、実際に要素を列挙するまで処理は実行されません。

クエリ式のwhere句との違い

LINQには、メソッドチェーンで記述する拡張メソッドスタイルと、SQLに似たクエリ式スタイルの2種類の書き方があります。

Whereメソッドは拡張メソッドスタイルでのフィルタリングを行いますが、クエリ式ではwhere句を使って同様の処理が可能です。

例えば、先ほどの偶数抽出をクエリ式で書くと以下のようになります。

int[] numbers = { 1, 2, 3, 4, 5, 6 };
var evenNumbers = from n in numbers
                  where n % 2 == 0
                  select n;
foreach (var num in evenNumbers)
{
    Console.WriteLine(num);
}

クエリ式はfromでデータソースを指定し、where句で条件を記述、selectで結果の形を指定します。

拡張メソッドスタイルと比べてSQLに近い構文なので、SQLに慣れている方には直感的に理解しやすいです。

ただし、クエリ式は内部的には拡張メソッドに変換されるため、パフォーマンスや動作に大きな違いはありません。

好みや可読性に応じて使い分けるとよいでしょう。

遅延実行とイテレーションのタイミング

LINQのWhereメソッドは遅延実行(Lazy Evaluation)を採用しています。

これは、Whereを呼び出した時点ではまだ実際のフィルタリング処理は行われず、結果のシーケンスが列挙される(foreachなどで要素を取り出す)タイミングで初めて処理が実行されることを意味します。

この遅延実行のメリットは以下の通りです。

  • パフォーマンスの向上

不要な処理を避け、必要な要素だけを効率的に取得できます。

例えば、Whereで絞り込んだ後にTake(5)で最初の5件だけ取得する場合、全件を処理せずに済みます。

  • メモリ効率の改善

フィルタリング結果をすぐにリスト化しないため、大量データでもメモリ消費を抑えられます。

  • 柔軟なクエリ構築

複数のLINQメソッドを連結しても、最終的に列挙されるまで処理は実行されません。

これにより、動的に条件を追加したり変更したりすることが容易です。

ただし、遅延実行には注意点もあります。

例えば、元のコレクションが列挙されるまでに変更されると、結果に影響が出ることがあります。

また、例外が発生するのも列挙時なので、エラーハンドリングのタイミングが変わることを理解しておく必要があります。

以下のサンプルコードで遅延実行の動作を確認してみましょう。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
        // Whereメソッドで偶数を抽出(まだ実行されていない)
        var evenNumbers = numbers.Where(n => n % 2 == 0);
        // 元のリストに要素を追加
        numbers.Add(6);
        // 列挙時にフィルタリング処理が実行される
        foreach (var num in evenNumbers)
        {
            Console.WriteLine(num);
        }
    }
}
2
4
6

この例では、Whereの呼び出し時点ではまだ処理は実行されていません。

numbers6を追加した後にevenNumbersを列挙すると、追加した6も含めて偶数が抽出されていることがわかります。

このように、LINQのWhereメソッドは遅延実行の特性を持つため、処理のタイミングや元データの変更に注意しながら使うことが重要です。

必要に応じてToList()ToArray()で即時実行(強制評価)することも検討してください。

単純条件での抽出

数値の偶奇判定

数値のコレクションから偶数や奇数を抽出するのは、LINQの基本的な使い方の一つです。

Whereメソッドに条件式を渡すだけで簡単に実現できます。

以下のサンプルコードでは、整数の配列から偶数だけを抽出しています。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        // 偶数を抽出
        var evenNumbers = numbers.Where(n => n % 2 == 0);
        Console.WriteLine("偶数の一覧:");
        foreach (var num in evenNumbers)
        {
            Console.WriteLine(num);
        }
    }
}
偶数の一覧:
2
4
6
8
10

このコードでは、n % 2 == 0という条件で割り切れる数だけを抽出しています。

奇数を抽出したい場合は、n % 2 != 0に変更すればよいです。

また、偶数・奇数の判定は整数型に限らず、longshortなどの整数型でも同様に使えます。

文字列の部分一致・前方一致・後方一致

文字列のコレクションから特定の文字列を含む要素を抽出する場合、Whereメソッドの条件式にContainsStartsWithEndsWithメソッドを使います。

これらは部分一致、前方一致、後方一致の判定に便利です。

以下のサンプルでは、文字列のリストから「apple」を含む要素、先頭が「b」で始まる要素、末尾が「e」で終わる要素をそれぞれ抽出しています。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        List<string> fruits = new List<string>
        {
            "apple",
            "banana",
            "grape",
            "pineapple",
            "blueberry",
            "orange"
        };
        // 部分一致: "apple"を含む
        var containsApple = fruits.Where(f => f.Contains("apple"));
        // 前方一致: "b"で始まる
        var startsWithB = fruits.Where(f => f.StartsWith("b"));
        // 後方一致: "e"で終わる
        var endsWithE = fruits.Where(f => f.EndsWith("e"));
        Console.WriteLine("「apple」を含む要素:");
        foreach (var fruit in containsApple)
        {
            Console.WriteLine(fruit);
        }
        Console.WriteLine("\n「b」で始まる要素:");
        foreach (var fruit in startsWithB)
        {
            Console.WriteLine(fruit);
        }
        Console.WriteLine("\n「e」で終わる要素:");
        foreach (var fruit in endsWithE)
        {
            Console.WriteLine(fruit);
        }
    }
}
「apple」を含む要素:
apple
pineapple

「b」で始まる要素:
banana
blueberry

「e」で終わる要素:
apple
grape
pineapple
orange

Containsは部分文字列がどこかに含まれているかを判定し、StartsWithは文字列の先頭が指定した文字列と一致するか、EndsWithは末尾が一致するかを判定します。

これらは大文字・小文字を区別するため、必要に応じてStringComparison.OrdinalIgnoreCaseを指定して大文字小文字を無視することも可能です。

日付の範囲指定

日付データのコレクションから特定の期間内に該当する要素を抽出する場合は、DateTime型の比較演算子を使って範囲指定を行います。

以下のサンプルでは、DateTimeのリストから2023年1月1日から2023年3月31日までの期間に該当する日付を抽出しています。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        List<DateTime> dates = new List<DateTime>
        {
            new DateTime(2022, 12, 31),
            new DateTime(2023, 1, 15),
            new DateTime(2023, 2, 10),
            new DateTime(2023, 4, 1),
            new DateTime(2023, 3, 31)
        };
        DateTime startDate = new DateTime(2023, 1, 1);
        DateTime endDate = new DateTime(2023, 3, 31);
        // 範囲内の日付を抽出
        var filteredDates = dates.Where(d => d >= startDate && d <= endDate);
        Console.WriteLine("2023年1月1日から3月31日までの日付:");
        foreach (var date in filteredDates)
        {
            Console.WriteLine(date.ToString("yyyy-MM-dd"));
        }
    }
}
2023年1月1日から3月31日までの日付:
2023-01-15
2023-02-10
2023-03-31

この例では、d >= startDate && d <= endDateという条件で日付の範囲を指定しています。

DateTime型は比較演算子がオーバーロードされているため、大小比較が直感的に行えます。

また、DateOnly型を使う場合も同様に比較演算子で範囲指定が可能です。

日付の範囲抽出は、イベントの期間絞り込みやログの期間検索などでよく使われます。

複数条件の組み合わせ

AND条件での範囲・属性同時チェック

複数の条件をすべて満たす要素を抽出したい場合は、Whereメソッドの条件式内で論理積(AND)演算子&&を使います。

これにより、範囲指定や複数の属性を同時にチェックできます。

以下のサンプルでは、整数の配列から「3以上かつ7以下」の範囲にあり、かつ偶数である要素を抽出しています。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        // 3以上7以下かつ偶数の要素を抽出
        var filteredNumbers = numbers.Where(n => n >= 3 && n <= 7 && n % 2 == 0);
        Console.WriteLine("3以上7以下かつ偶数の数:");
        foreach (var num in filteredNumbers)
        {
            Console.WriteLine(num);
        }
    }
}
3以上7以下かつ偶数の数:
4
6

このように、&&を使うことで複数の条件をすべて満たす要素だけを抽出できます。

文字列やオブジェクトのプロパティに対しても同様に複数条件を組み合わせられます。

例えば、以下は商品リストから価格が1000円以上かつ在庫が10個以上の商品を抽出する例です。

using System;
using System.Collections.Generic;
using System.Linq;
class Product
{
    public string Name { get; set; }
    public int Price { get; set; }
    public int Stock { get; set; }
}
class Program
{
    static void Main()
    {
        var products = new List<Product>
        {
            new Product { Name = "ペン", Price = 500, Stock = 20 },
            new Product { Name = "ノート", Price = 1200, Stock = 5 },
            new Product { Name = "ファイル", Price = 1500, Stock = 15 },
            new Product { Name = "消しゴム", Price = 300, Stock = 50 }
        };
        var filteredProducts = products.Where(p => p.Price >= 1000 && p.Stock >= 10);
        Console.WriteLine("価格が1000円以上かつ在庫が10個以上の商品:");
        foreach (var product in filteredProducts)
        {
            Console.WriteLine($"{product.Name} - 価格: {product.Price}円, 在庫: {product.Stock}個");
        }
    }
}
価格が1000円以上かつ在庫が10個以上の商品:
ファイル - 価格: 1500円, 在庫: 15個

OR条件での条件緩和

条件のいずれかを満たす要素を抽出したい場合は、論理和(OR)演算子||を使います。

これにより、複数の条件のうちどれか一つでも真であれば結果に含めることができます。

以下のサンプルでは、整数の配列から「3未満または7より大きい」要素を抽出しています。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        // 3未満または7より大きい要素を抽出
        var filteredNumbers = numbers.Where(n => n < 3 || n > 7);
        Console.WriteLine("3未満または7より大きい数:");
        foreach (var num in filteredNumbers)
        {
            Console.WriteLine(num);
        }
    }
}
3未満または7より大きい数:
1
2
8
9
10

文字列の例では、名前が「田中」または「佐藤」で始まる要素を抽出することも可能です。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var names = new List<string> { "田中一郎", "佐藤花子", "鈴木次郎", "高橋三郎" };
        var filteredNames = names.Where(name => name.StartsWith("田中") || name.StartsWith("佐藤"));
        Console.WriteLine("「田中」または「佐藤」で始まる名前:");
        foreach (var name in filteredNames)
        {
            Console.WriteLine(name);
        }
    }
}
「田中」または「佐藤」で始まる名前:
田中一郎
佐藤花子

条件式の外部メソッド化と再利用

複雑な条件式をWhereのラムダ式内に直接書くと可読性が低下し、保守が難しくなります。

そこで、条件判定のロジックを外部メソッドに切り出して再利用する方法が有効です。

以下の例では、商品が「価格が1000円以上かつ在庫が10個以上」という条件を判定するメソッドIsAvailableProductを定義し、Whereの引数に渡しています。

using System;
using System.Collections.Generic;
using System.Linq;
class Product
{
    public string Name { get; set; }
    public int Price { get; set; }
    public int Stock { get; set; }
}
class Program
{
    static bool IsAvailableProduct(Product p)
    {
        // 価格が1000円以上かつ在庫が10個以上かどうかを判定
        return p.Price >= 1000 && p.Stock >= 10;
    }
    static void Main()
    {
        var products = new List<Product>
        {
            new Product { Name = "ペン", Price = 500, Stock = 20 },
            new Product { Name = "ノート", Price = 1200, Stock = 5 },
            new Product { Name = "ファイル", Price = 1500, Stock = 15 },
            new Product { Name = "消しゴム", Price = 300, Stock = 50 }
        };
        var filteredProducts = products.Where(IsAvailableProduct);
        Console.WriteLine("条件を外部メソッド化した商品抽出:");
        foreach (var product in filteredProducts)
        {
            Console.WriteLine($"{product.Name} - 価格: {product.Price}円, 在庫: {product.Stock}個");
        }
    }
}
条件を外部メソッド化した商品抽出:
ファイル - 価格: 1500円, 在庫: 15個

この方法のメリットは以下の通りです。

  • 条件ロジックがメソッドにまとまるため、コードの見通しが良くなる
  • 複数箇所で同じ条件を使い回せる
  • 単体テストがしやすくなる

ラムダ式の中に複雑な条件を詰め込むのではなく、外部メソッドに切り出して可読性と保守性を高めることをおすすめします。

インデックスを用いたフィルタリング

Where((value, index) => …)の活用

LINQのWhereメソッドは、単に要素の値だけでなく、その要素のインデックス(位置)を利用した条件でフィルタリングすることも可能です。

これはWhereメソッドのオーバーロードで、引数に要素の値とインデックスの両方を受け取るラムダ式を指定します。

シグネチャは以下の通りです。

IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, int, bool> predicate)
  • TSourceは要素の型
  • predicateは要素の値とインデックスを受け取り、trueならその要素を結果に含めます

この機能を使うと、要素の位置に基づくフィルタリングが簡単にできます。

以下のサンプルでは、文字列のリストからインデックスが偶数の要素だけを抽出しています。

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

この例では、index % 2 == 0の条件で0, 2, 4番目の要素を抽出しています。

インデックスは0から始まるため、最初の要素がインデックス0に該当します。

奇数番目・偶数番目要素の抽出

インデックスを使ったフィルタリングは、奇数番目や偶数番目の要素を抽出する際に特に便利です。

ここでの「奇数番目」「偶数番目」は、0始まりのインデックスに基づくため、インデックスが偶数なら「1番目、3番目、5番目…」、インデックスが奇数なら「2番目、4番目、6番目…」の要素を指します。

以下のサンプルでは、整数配列から奇数番目(インデックスが奇数)と偶数番目(インデックスが偶数)の要素をそれぞれ抽出しています。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 10, 20, 30, 40, 50, 60, 70 };
        // 偶数番目の要素(インデックス0,2,4,...)
        var evenIndexNumbers = numbers.Where((num, index) => index % 2 == 0);
        // 奇数番目の要素(インデックス1,3,5,...)
        var oddIndexNumbers = numbers.Where((num, index) => index % 2 == 1);
        Console.WriteLine("偶数番目の要素:");
        foreach (var num in evenIndexNumbers)
        {
            Console.WriteLine(num);
        }
        Console.WriteLine("\n奇数番目の要素:");
        foreach (var num in oddIndexNumbers)
        {
            Console.WriteLine(num);
        }
    }
}
偶数番目の要素:
10
30
50
70

奇数番目の要素:
20
40
60

このように、Whereのインデックス引数を活用することで、位置に基づく抽出が簡単に行えます。

例えば、リストの偶数番目だけを処理したい場合や、交互に要素を取り出したい場合などに役立ちます。

Nullと例外への配慮

Null許容型の安全な比較

LINQでフィルタリングを行う際、対象のコレクションや要素にnullが含まれている場合があります。

特に文字列や参照型のプロパティを条件に使うときは、null参照による例外NullReferenceExceptionを防ぐために安全な比較を行うことが重要です。

例えば、文字列のリストから「apple」を含む要素を抽出する場合、要素がnullだとContainsメソッドを呼び出した時に例外が発生します。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        List<string> fruits = new List<string> { "apple", null, "pineapple", "banana" };
        // nullチェックなしでContainsを使うと例外が発生する可能性がある
        // var filtered = fruits.Where(f => f.Contains("apple")); // 例外発生
        // 安全な比較: nullチェックを先に行う
        var filtered = fruits.Where(f => f != null && f.Contains("apple"));
        Console.WriteLine("「apple」を含む要素(null安全):");
        foreach (var fruit in filtered)
        {
            Console.WriteLine(fruit);
        }
    }
}
「apple」を含む要素(null安全):
apple
pineapple

このように、f != nullのチェックを先に行うことで、null要素を除外し安全にContainsを呼び出せます。

C# 6.0以降では、null条件演算子?.を使ってさらに簡潔に書くことも可能です。

var filtered = fruits.Where(f => f?.Contains("apple") == true);

この書き方は、fnullの場合はnullを返し、== trueで真偽値を判定するため、null要素は自動的に除外されます。

また、null許容型の値型(int?DateTime?など)を比較する場合も、HasValueプロパティやGetValueOrDefaultメソッドを使って安全に扱うことが推奨されます。

コレクションがNullの場合のガード節

LINQのWhereメソッドは、対象のコレクションがnullの場合に呼び出すとArgumentNullExceptionが発生します。

したがって、コレクション自体がnullの可能性がある場合は、事前にnullチェックを行うか、空のコレクションに置き換えてから処理を行うことが重要です。

以下の例では、itemsnullの場合に備えてガード節を設けています。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        List<string> items = null;
        // nullチェックを行い、nullなら空のリストに置き換える
        var safeItems = items ?? Enumerable.Empty<string>();
        var filtered = safeItems.Where(item => item != null && item.StartsWith("A"));
        Console.WriteLine("「A」で始まる要素:");
        foreach (var item in filtered)
        {
            Console.WriteLine(item);
        }
    }
}
「A」で始まる要素:

この例では、itemsnullのためEnumerable.Empty<string>()で空のシーケンスに置き換えています。

これにより、Whereメソッドの呼び出し時に例外が発生せず、安全に処理が進みます。

また、メソッドの引数としてコレクションを受け取る場合は、呼び出し元でnullチェックを行うか、メソッド内で以下のようにガード節を設けることが一般的です。

void ProcessItems(IEnumerable<string> items)
{
    if (items == null)
    {
        Console.WriteLine("コレクションがnullです。処理を中断します。");
        return;
    }
    var filtered = items.Where(item => item != null && item.Length > 0);
    // 処理続行...
}

このように、コレクションがnullである可能性を考慮し、適切にガード節を設けることで例外を防ぎ、堅牢なコードを書くことができます。

文字列特化のフィルタリング

大文字小文字を無視した比較

文字列のフィルタリングで大文字・小文字の違いを無視したい場合、Stringクラスの比較メソッドにStringComparison列挙体を指定する方法が一般的です。

Whereメソッドの条件式内でEqualsStartsWithEndsWithContainsなどのメソッドにStringComparison.OrdinalIgnoreCaseStringComparison.InvariantCultureIgnoreCaseを渡すことで、大文字小文字を区別せずに比較できます。

以下のサンプルでは、文字列リストから「apple」という単語を大文字小文字を無視して含む要素を抽出しています。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var fruits = new List<string>
        {
            "Apple",
            "banana",
            "APPLE pie",
            "PineApple",
            "orange"
        };
        // 大文字小文字を無視して"apple"を含む要素を抽出
        var filtered = fruits.Where(f => f.IndexOf("apple", StringComparison.OrdinalIgnoreCase) >= 0);
        Console.WriteLine("「apple」を大文字小文字無視で含む要素:");
        foreach (var fruit in filtered)
        {
            Console.WriteLine(fruit);
        }
    }
}
「apple」を大文字小文字無視で含む要素:
Apple
APPLE pie
PineApple

IndexOfメソッドは、指定した文字列が最初に現れる位置を返します。

見つからなければ-1を返すため、>= 0で含むかどうかを判定しています。

StringComparison.OrdinalIgnoreCaseを指定することで、大文字小文字を区別しません。

同様に、StartsWithEndsWithも以下のように使えます。

var startsWithA = fruits.Where(f => f.StartsWith("a", StringComparison.OrdinalIgnoreCase));
var endsWithE = fruits.Where(f => f.EndsWith("e", StringComparison.OrdinalIgnoreCase));

これらのメソッドを使うことで、文字列の大文字小文字を気にせず柔軟にフィルタリングできます。

正規表現Regex.IsMatchとの併用

より複雑な文字列パターンでフィルタリングしたい場合は、System.Text.RegularExpressions.RegexクラスのIsMatchメソッドを使うと便利です。

正規表現を使うことで、部分一致だけでなく、パターンマッチングや文字種の指定、繰り返しなど多彩な条件を表現できます。

以下のサンプルでは、文字列リストから「apple」または「banana」を含む要素を正規表現で抽出しています。

大文字小文字を無視するためにRegexOptions.IgnoreCaseを指定しています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        var fruits = new List<string>
        {
            "Apple",
            "banana",
            "Cherry",
            "Pineapple",
            "Banana Split",
            "Orange"
        };
        var pattern = "apple|banana"; // "apple"または"banana"にマッチ
        var filtered = fruits.Where(f => Regex.IsMatch(f, pattern, RegexOptions.IgnoreCase));
        Console.WriteLine("正規表現で「apple」または「banana」を含む要素:");
        foreach (var fruit in filtered)
        {
            Console.WriteLine(fruit);
        }
    }
}
正規表現で「apple」または「banana」を含む要素:
Apple
banana
Pineapple
Banana Split

正規表現は柔軟性が高い反面、パフォーマンスに影響を与えることがあるため、単純な部分一致で十分な場合はIndexOfContainsの方が高速です。

複雑なパターンマッチングが必要な場合にRegexを使うとよいでしょう。

また、正規表現のパターンは事前にコンパイルしておくとパフォーマンスが向上します。

var regex = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled);
var filtered = fruits.Where(f => regex.IsMatch(f));

このように、Regex.IsMatchとLINQのWhereを組み合わせることで、強力かつ柔軟な文字列フィルタリングが可能になります。

日付・時刻データの扱い

DateTimeとDateOnlyによる期間絞り込み

C#で日付や時刻を扱う際、DateTime型は日時情報を持ち、時刻まで含めて管理できます。

一方、DateOnly型は日付部分のみを扱うため、時刻を無視した期間絞り込みに便利です。

LINQのWhereメソッドを使って、これらの型で期間を指定してデータを抽出する方法を紹介します。

まず、DateTimeを使った期間絞り込みの例です。

以下のサンプルでは、日時のリストから2023年1月1日0時0分0秒以降かつ2023年1月31日23時59分59秒以前のデータを抽出しています。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var dateTimes = new List<DateTime>
        {
            new DateTime(2022, 12, 31, 23, 59, 59),
            new DateTime(2023, 1, 1, 0, 0, 0),
            new DateTime(2023, 1, 15, 12, 30, 0),
            new DateTime(2023, 1, 31, 23, 59, 59),
            new DateTime(2023, 2, 1, 0, 0, 0)
        };
        DateTime start = new DateTime(2023, 1, 1, 0, 0, 0);
        DateTime end = new DateTime(2023, 1, 31, 23, 59, 59);
        var filtered = dateTimes.Where(dt => dt >= start && dt <= end);
        Console.WriteLine("2023年1月の日時データ:");
        foreach (var dt in filtered)
        {
            Console.WriteLine(dt.ToString("yyyy-MM-dd HH:mm:ss"));
        }
    }
}
2023年1月の日時データ:
2023-01-01 00:00:00
2023-01-15 12:30:00
2023-01-31 23:59:59

DateTimeは時刻まで含むため、時間単位での絞り込みが可能です。

次に、DateOnlyを使った例です。

DateOnlyは.NET 6以降で利用可能で、日付だけを扱うため、時刻を気にせずに期間を指定できます。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var dates = new List<DateOnly>
        {
            new DateOnly(2023, 1, 1),
            new DateOnly(2023, 1, 15),
            new DateOnly(2023, 1, 31),
            new DateOnly(2023, 2, 1)
        };
        DateOnly start = new DateOnly(2023, 1, 1);
        DateOnly end = new DateOnly(2023, 1, 31);
        var filtered = dates.Where(d => d >= start && d <= end);
        Console.WriteLine("2023年1月の日付データ:");
        foreach (var d in filtered)
        {
            Console.WriteLine(d.ToString("yyyy-MM-dd"));
        }
    }
}
2023年1月の日付データ:
2023-01-01
2023-01-15
2023-01-31

DateOnlyは日付単位の比較に特化しているため、時刻の影響を受けずに期間絞り込みができます。

DateTimeDateOnlyは用途に応じて使い分けるとよいでしょう。

タイムゾーン変換を伴う条件指定

日時データを扱う際、タイムゾーンの違いを考慮する必要がある場合があります。

特にサーバーのローカル時間とユーザーのタイムゾーンが異なる場合、正確な期間絞り込みにはタイムゾーン変換が欠かせません。

DateTime型にはKindプロパティがあり、Utc(協定世界時)、Local(ローカル時間)、Unspecified(未指定)の3種類があります。

タイムゾーンを考慮した比較を行うには、日時を共通のタイムゾーンに変換してから比較するのが基本です。

以下のサンプルでは、UTC日時のリストを日本標準時(JST、UTC+9)に変換し、JSTの2023年1月1日から1月31日までの期間に該当する日時を抽出しています。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var utcDates = new List<DateTime>
        {
            new DateTime(2022, 12, 31, 15, 0, 0, DateTimeKind.Utc), // JST: 2023-01-01 00:00:00
            new DateTime(2023, 1, 10, 3, 0, 0, DateTimeKind.Utc),  // JST: 2023-01-10 12:00:00
            new DateTime(2023, 2, 1, 0, 0, 0, DateTimeKind.Utc)    // JST: 2023-02-01 09:00:00
        };
        // JSTのタイムゾーン情報を取得
        TimeZoneInfo jstZone = TimeZoneInfo.FindSystemTimeZoneById("Tokyo Standard Time");
        DateTime jstStart = new DateTime(2023, 1, 1, 0, 0, 0);
        DateTime jstEnd = new DateTime(2023, 1, 31, 23, 59, 59);
        var filtered = utcDates.Where(utc =>
        {
            // UTC日時をJSTに変換
            DateTime jstTime = TimeZoneInfo.ConvertTimeFromUtc(utc, jstZone);
            return jstTime >= jstStart && jstTime <= jstEnd;
        });
        Console.WriteLine("JSTの2023年1月の日時データ:");
        foreach (var utc in filtered)
        {
            DateTime jstTime = TimeZoneInfo.ConvertTimeFromUtc(utc, jstZone);
            Console.WriteLine(jstTime.ToString("yyyy-MM-dd HH:mm:ss"));
        }
    }
}
JSTの2023年1月の日時データ:
2023-01-01 00:00:00
2023-01-10 12:00:00

この例では、UTCの日時をJSTに変換してから期間判定を行っています。

Windows環境ではタイムゾーンIDが"Tokyo Standard Time"ですが、LinuxやmacOSでは"Asia/Tokyo"など異なる場合があるため、環境に応じて適切なIDを指定してください。

また、DateTimeOffset型を使うとタイムゾーン情報を含む日時を扱いやすくなります。

DateTimeOffsetは日時とオフセット(UTCとの差)を持つため、変換や比較がより明確になります。

タイムゾーンを考慮した期間絞り込みは、グローバルなアプリケーションや多地域対応のシステムで特に重要です。

日時のKindやタイムゾーン変換を正しく理解し、適切に処理を行いましょう。

複合オブジェクトの条件抽出

プロパティ深掘りと匿名型投影

LINQで複合オブジェクトの条件抽出を行う際、オブジェクトのプロパティを深掘りして条件を指定することがよくあります。

また、必要に応じて匿名型を使って特定のプロパティだけを抽出・投影(プロジェクション)することも可能です。

以下の例では、Personクラスの中にAddressという複合プロパティがあり、その中のCityプロパティを条件にフィルタリングしています。

また、結果は匿名型で名前と都市名だけを抽出しています。

using System;
using System.Collections.Generic;
using System.Linq;
class Address
{
    public string City { get; set; }
    public string Street { get; set; }
}
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public Address Address { get; set; }
}
class Program
{
    static void Main()
    {
        var people = new List<Person>
        {
            new Person { Name = "山田太郎", Age = 30, Address = new Address { City = "東京", Street = "千代田区" } },
            new Person { Name = "佐藤花子", Age = 25, Address = new Address { City = "大阪", Street = "北区" } },
            new Person { Name = "鈴木次郎", Age = 40, Address = new Address { City = "東京", Street = "港区" } }
        };
        // 東京に住む人を抽出し、名前と都市名だけを匿名型で取得
        var filtered = people
            .Where(p => p.Address != null && p.Address.City == "東京")
            .Select(p => new { p.Name, City = p.Address.City });
        Console.WriteLine("東京に住む人:");
        foreach (var person in filtered)
        {
            Console.WriteLine($"名前: {person.Name}, 都市: {person.City}");
        }
    }
}
東京に住む人:
名前: 山田太郎, 都市: 東京
名前: 鈴木次郎, 都市: 東京

この例では、Where句でAddressnullでないことを確認しつつ、Cityが「東京」であるかを判定しています。

Select句で匿名型を使い、必要な情報だけを抽出しているため、後続の処理や表示がシンプルになります。

匿名型は型名を定義せずに複数のプロパティをまとめて扱えるため、データの一時的な投影に非常に便利です。

ネストしたコレクションでのAny/All検索

複合オブジェクトの中にさらにコレクションがネストしている場合、LINQのAnyAllメソッドを使って条件を指定することが多いです。

Anyはコレクション内に条件を満たす要素が1つでもあればtrueを返し、Allはすべての要素が条件を満たす場合にtrueを返します。

以下の例では、Orderクラスの中に複数のOrderItemがあり、注文の中に特定の商品が含まれているかどうかをAnyで判定しています。

using System;
using System.Collections.Generic;
using System.Linq;
class OrderItem
{
    public string ProductName { get; set; }
    public int Quantity { get; set; }
}
class Order
{
    public int OrderId { get; set; }
    public List<OrderItem> Items { get; set; }
}
class Program
{
    static void Main()
    {
        var orders = new List<Order>
        {
            new Order
            {
                OrderId = 1,
                Items = new List<OrderItem>
                {
                    new OrderItem { ProductName = "ペン", Quantity = 3 },
                    new OrderItem { ProductName = "ノート", Quantity = 5 }
                }
            },
            new Order
            {
                OrderId = 2,
                Items = new List<OrderItem>
                {
                    new OrderItem { ProductName = "消しゴム", Quantity = 2 }
                }
            },
            new Order
            {
                OrderId = 3,
                Items = new List<OrderItem>
                {
                    new OrderItem { ProductName = "ペン", Quantity = 1 },
                    new OrderItem { ProductName = "定規", Quantity = 4 }
                }
            }
        };
        // 注文の中に「ペン」が含まれている注文を抽出
        var ordersWithPen = orders.Where(o => o.Items != null && o.Items.Any(item => item.ProductName == "ペン"));
        Console.WriteLine("「ペン」が含まれる注文ID:");
        foreach (var order in ordersWithPen)
        {
            Console.WriteLine(order.OrderId);
        }
    }
}
「ペン」が含まれる注文ID:
1
3

この例では、Whereの条件でItemsnullでないことを確認し、Anyを使ってProductNameが「ペン」であるアイテムが存在するかを判定しています。

同様に、すべてのアイテムが特定の条件を満たすかを調べたい場合はAllを使います。

例えば、以下はすべての注文アイテムの数量が1以上である注文を抽出する例です。

var validOrders = orders.Where(o => o.Items != null && o.Items.All(item => item.Quantity > 0));

AnyAllを使うことで、ネストしたコレクションの中身に対して柔軟に条件を指定でき、複雑なデータ構造でも簡潔にフィルタリングが可能です。

コレクション種類別のアプローチ

List<T>とIEnumerable<T>の違いを意識した記述

C#のLINQでよく使われるコレクション型として、List<T>IEnumerable<T>があります。

これらは似ているようで異なる特徴を持つため、フィルタリングや条件式を書く際には違いを理解しておくことが重要です。

List<T>は可変長のリストであり、要素の追加・削除が可能な具体的なコレクションです。

一方、IEnumerable<T>は列挙可能なシーケンスを表すインターフェースで、実際のデータ構造を隠蔽し、遅延実行をサポートします。

LINQのWhereメソッドはIEnumerable<T>に対して定義されているため、List<T>IEnumerable<T>として扱えますが、以下の点に注意が必要です。

  • 遅延実行の影響

IEnumerable<T>は遅延実行を行うため、Whereで条件を指定しても即座に処理は実行されません。

List<T>のメソッド(例:CountAdd)は即時実行です。

そのため、IEnumerable<T>のまま処理を続けると、複数回列挙される可能性があり、パフォーマンスに影響を与えることがあります。

  • メソッドの利用制限

List<T>はインデックスアクセスや要素の追加・削除が可能ですが、IEnumerable<T>は読み取り専用でインデックスアクセスができません。

そのため、条件式の中でインデックスを使いたい場合はList<T>や配列などの具体的なコレクションが必要です。

  • メモリ効率

IEnumerable<T>は必要な要素だけを逐次処理できるため、大量データの処理に向いています。

List<T>は全要素をメモリに保持します。

以下のサンプルでは、List<int>IEnumerable<int>で同じ条件を使ったフィルタリングを行い、ToList()で即時実行している例です。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        List<int> listNumbers = new List<int> { 1, 2, 3, 4, 5 };
        IEnumerable<int> enumerableNumbers = listNumbers;
        // IEnumerableのままフィルタリング(遅延実行)
        var filteredEnumerable = enumerableNumbers.Where(n => n > 2);
        // Listに変換して即時実行
        var filteredList = filteredEnumerable.ToList();
        Console.WriteLine("IEnumerableでフィルタリング後、ToListで即時実行:");
        foreach (var num in filteredList)
        {
            Console.WriteLine(num);
        }
    }
}
IEnumerableでフィルタリング後、ToListで即時実行:
3
4
5

このように、IEnumerable<T>のまま処理を続ける場合は遅延実行の特性を意識し、必要に応じてToList()ToArray()で即時実行することが望ましいです。

配列・HashSet<T>・Dictionary<TKey,TValue>での条件式例

LINQは配列やHashSet<T>Dictionary<TKey,TValue>など様々なコレクションに対応していますが、条件式の書き方や注意点が少し異なります。

配列での条件式例

配列は固定長のコレクションで、インデックスアクセスが可能です。

WhereメソッドはIEnumerable<T>として使えます。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 10, 20, 30, 40, 50 };
        // 30以上の要素を抽出
        var filtered = numbers.Where(n => n >= 30);
        Console.WriteLine("配列から30以上の数:");
        foreach (var num in filtered)
        {
            Console.WriteLine(num);
        }
    }
}
配列から30以上の数:
30
40
50

HashSet<T>での条件式例

HashSet<T>は重複を許さない集合で、要素の順序は保証されません。

LINQのWhereは問題なく使えますが、順序を意識した処理は避けるべきです。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var set = new HashSet<string> { "apple", "banana", "cherry" };
        // "a"を含む要素を抽出
        var filtered = set.Where(s => s.Contains("a"));
        Console.WriteLine("HashSetから'a'を含む要素:");
        foreach (var item in filtered)
        {
            Console.WriteLine(item);
        }
    }
}
HashSetから'a'を含む要素:
apple
banana

順序は保証されないため、結果の順序に依存する処理は注意してください。

Dictionary<TKey,TValue>での条件式例

Dictionary<TKey,TValue>はキーと値のペアを保持するコレクションです。

LINQではKeyValuePair<TKey,TValue>型のシーケンスとして扱われます。

条件式ではキーや値のどちらか、または両方を使ってフィルタリングできます。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var dict = new Dictionary<int, string>
        {
            { 1, "東京" },
            { 2, "大阪" },
            { 3, "名古屋" }
        };
        // キーが2以上の要素を抽出
        var filteredByKey = dict.Where(kv => kv.Key >= 2);
        Console.WriteLine("キーが2以上の要素:");
        foreach (var kv in filteredByKey)
        {
            Console.WriteLine($"キー: {kv.Key}, 値: {kv.Value}");
        }
        // 値に「大」が含まれる要素を抽出
        var filteredByValue = dict.Where(kv => kv.Value.Contains("大"));
        Console.WriteLine("\n値に「大」が含まれる要素:");
        foreach (var kv in filteredByValue)
        {
            Console.WriteLine($"キー: {kv.Key}, 値: {kv.Value}");
        }
    }
}
キーが2以上の要素:
キー: 2, 値: 大阪
キー: 3, 値: 名古屋
値に「大」が含まれる要素:
キー: 2, 値: 大阪
キー: 3, 値: 名古屋

このように、DictionaryKeyValuePairKeyValueを使って柔軟に条件を指定できます。

コレクションの種類によって特性や使い方が異なるため、LINQの条件式を書く際は対象のコレクションの特徴を理解し、適切な方法でフィルタリングを行うことが重要です。

パフォーマンス最適化

不要なToList()呼び出しを避ける方法

LINQのクエリを実行する際、ToList()メソッドを使うことでIEnumerable<T>の遅延実行を即時実行に変え、結果をリストとして取得できます。

しかし、ToList()を多用すると不要なリストのコピーやメモリ消費が発生し、パフォーマンス低下の原因となります。

例えば、以下のように複数回ToList()を呼び出すケースは避けるべきです。

var filtered = source.Where(x => x.IsActive).ToList();
var furtherFiltered = filtered.Where(x => x.Score > 50).ToList();

この場合、最初のToList()で一度リスト化し、次のWhereでさらに絞り込んで再度リスト化しています。

これにより、2回のリスト生成が発生し、無駄なメモリ消費と処理時間がかかります。

パフォーマンスを最適化するには、可能な限りToList()の呼び出しを遅らせ、クエリをまとめてから一度だけ実行することが重要です。

var filtered = source.Where(x => x.IsActive)
                     .Where(x => x.Score > 50)
                     .ToList();

このようにクエリを合成してからToList()を呼ぶことで、1回のリスト生成で済み、効率的に処理できます。

クエリ合成による一括評価

LINQは遅延実行の特性を持つため、複数の条件を連結してクエリを合成できます。

これにより、データソースを一度だけ列挙して条件をまとめて評価できるため、パフォーマンスが向上します。

例えば、以下のように複数のWhereを連結しても、実際のデータ列挙はforeachToList()などで結果を取得するタイミングまで遅延されます。

var query = source.Where(x => x.IsActive)
                  .Where(x => x.Score > 50)
                  .Where(x => x.Category == "A");

このクエリは、sourceを一度だけ列挙し、各要素に対して3つの条件を順に評価します。

途中で条件を満たさない要素はすぐに除外されるため、無駄な処理が減ります。

クエリ合成のポイントは以下の通りです。

  • 条件は可能な限りまとめて書くか、複数のWhereで分割しても問題ない
  • クエリの実行は結果を列挙するタイミング(foreachToList()など)で一度だけ行われる
  • 不要な中間コレクションの生成を避けられる

この特性を活かして、複雑な条件でも効率的にフィルタリングを行いましょう。

多重列挙を防ぐToArray()の挿入ポイント

LINQのIEnumerable<T>は遅延実行のため、同じクエリを複数回列挙すると、その都度データソースを走査します。

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

例えば、以下のコードはqueryを2回列挙しているため、2回データソースを走査します。

var query = source.Where(x => x.IsActive);
int count = query.Count();
var list = query.ToList();

この場合、Count()ToList()の両方でqueryが列挙されます。

多重列挙を防ぐには、ToArray()ToList()を使って一度だけ列挙し、結果をメモリ上に保持する方法があります。

ToArray()は配列を返し、ToList()はリストを返しますが、どちらも即時実行を行います。

var array = source.Where(x => x.IsActive).ToArray();
int count = array.Length;
var list = array.ToList();

このように、ToArray()で一度だけ列挙して配列に格納すれば、その後の操作は配列に対して行われるため、多重列挙を防げます。

ToArray()を使うタイミングは、以下のような場合が多いです。

  • 同じクエリ結果を複数回使う場合
  • データソースが遅延実行で副作用がある場合
  • パフォーマンスを重視し、複数回の列挙を避けたい場合

ただし、ToArray()ToList()はメモリに全要素を保持するため、大量データの場合はメモリ消費に注意してください。

これらのポイントを踏まえ、LINQのパフォーマンスを最適化することで、効率的かつ安定したデータ処理が可能になります。

条件式の動的生成

Func<T,bool>デリゲートの組み立て

LINQのWhereメソッドでは、条件式をラムダ式として直接記述することが多いですが、動的に条件を組み立てたい場合はFunc<T,bool>デリゲートを使って条件式を生成し、柔軟に切り替えることが可能です。

例えば、ユーザーの入力や設定に応じてフィルタ条件を変えたい場合、複数の条件を組み合わせてFunc<T,bool>を作成し、それをWhereに渡す方法が有効です。

以下のサンプルでは、Personクラスのリストから年齢や名前の条件を動的に組み立ててフィルタリングしています。

using System;
using System.Collections.Generic;
using System.Linq;
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
class Program
{
    static void Main()
    {
        var people = new List<Person>
        {
            new Person { Name = "山田太郎", Age = 25 },
            new Person { Name = "佐藤花子", Age = 30 },
            new Person { Name = "鈴木次郎", Age = 35 }
        };
        // 動的に条件を組み立てる例
        bool filterByAge = true;
        int minAge = 28;
        bool filterByName = true;
        string nameContains = "佐藤";
        Func<Person, bool> predicate = p => true; // 初期は常にtrue
        if (filterByAge)
        {
            var prev = predicate;
            predicate = p => prev(p) && p.Age >= minAge;
        }
        if (filterByName)
        {
            var prev = predicate;
            predicate = p => prev(p) && p.Name.Contains(nameContains);
        }
        var filtered = people.Where(predicate);
        Console.WriteLine("動的条件でフィルタリングした結果:");
        foreach (var person in filtered)
        {
            Console.WriteLine($"{person.Name} - {person.Age}歳");
        }
    }
}
動的条件でフィルタリングした結果:
佐藤花子 - 30歳

この例では、predicateを初期化してから条件ごとに前の条件を保持しつつ新しい条件を追加しています。

こうすることで、条件のON/OFFやパラメータの変更に柔軟に対応できます。

Expression<Func<T,bool>>での後処理対応

Func<T,bool>はメソッドとして実行可能なデリゲートですが、式ツリー(Expression Tree)としての情報は持ちません。

データベースクエリやORM(Entity Frameworkなど)でLINQを使う場合、式ツリーを扱うExpression<Func<T,bool>>が必要です。

これにより、クエリの解析や変換、最適化が可能になります。

動的に条件式を組み立てる際にExpression<Func<T,bool>>を使うと、後で式ツリーを解析したり、他の式と合成したりできます。

以下のサンプルは、Expression<Func<T,bool>>を使って動的に条件を組み立て、Whereに渡す例です。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
class Program
{
    static void Main()
    {
        var people = new List<Person>
        {
            new Person { Name = "山田太郎", Age = 25 },
            new Person { Name = "佐藤花子", Age = 30 },
            new Person { Name = "鈴木次郎", Age = 35 }
        };
        bool filterByAge = true;
        int minAge = 28;
        bool filterByName = true;
        string nameContains = "佐藤";
        // 初期の式は常にtrueを返す式
        Expression<Func<Person, bool>> predicate = p => true;
        if (filterByAge)
        {
            Expression<Func<Person, bool>> ageExpr = p => p.Age >= minAge;
            predicate = predicate.AndAlso(ageExpr);
        }
        if (filterByName)
        {
            Expression<Func<Person, bool>> nameExpr = p => p.Name.Contains(nameContains);
            predicate = predicate.AndAlso(nameExpr);
        }
        var filtered = people.AsQueryable().Where(predicate);
        Console.WriteLine("Expressionで動的条件を組み立てた結果:");
        foreach (var person in filtered)
        {
            Console.WriteLine($"{person.Name} - {person.Age}歳");
        }
    }
}
// ExpressionのAndAlso拡張メソッド
public static class ExpressionExtensions
{
    public static Expression<Func<T, bool>> AndAlso<T>(
        this Expression<Func<T, bool>> expr1,
        Expression<Func<T, bool>> expr2)
    {
        var parameter = Expression.Parameter(typeof(T));
        var leftVisitor = new ReplaceParameterVisitor(expr1.Parameters[0], parameter);
        var left = leftVisitor.Visit(expr1.Body);
        var rightVisitor = new ReplaceParameterVisitor(expr2.Parameters[0], parameter);
        var right = rightVisitor.Visit(expr2.Body);
        var body = Expression.AndAlso(left, right);
        return Expression.Lambda<Func<T, bool>>(body, parameter);
    }
    private class ReplaceParameterVisitor : ExpressionVisitor
    {
        private readonly ParameterExpression _oldParameter;
        private readonly ParameterExpression _newParameter;
        public ReplaceParameterVisitor(ParameterExpression oldParameter, ParameterExpression newParameter)
        {
            _oldParameter = oldParameter;
            _newParameter = newParameter;
        }
        protected override Expression VisitParameter(ParameterExpression node)
        {
            return node == _oldParameter ? _newParameter : base.VisitParameter(node);
        }
    }
}
Expressionで動的条件を組み立てた結果:
佐藤花子 - 30歳

この例では、Expression<Func<T,bool>>の式ツリーをAndAlso拡張メソッドで合成しています。

ReplaceParameterVisitorは異なるパラメータを統一するために使われ、複数の式を正しく結合できるようにしています。

Expression<Func<T,bool>>を使うことで、ORMのクエリ変換やSQL生成などの後処理が可能になり、動的クエリの柔軟性とパフォーマンスを両立できます。

並列・非同期シナリオ

PLINQでの並列フィルタリング

PLINQ(Parallel LINQ)は、LINQクエリを並列処理に対応させるための拡張機能です。

大量のデータを複数のCPUコアで同時に処理することで、フィルタリングや集計のパフォーマンスを向上させることができます。

PLINQを使うには、AsParallel()メソッドを呼び出して並列クエリに変換し、その後に通常のLINQメソッドを続けて記述します。

並列処理は自動的にスレッドプールのスレッドを利用して実行されます。

以下のサンプルでは、1から1000までの整数の中から、3の倍数かつ5の倍数の数をPLINQで並列に抽出しています。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = Enumerable.Range(1, 1000);
        // PLINQで並列フィルタリング
        var filtered = numbers.AsParallel()
                              .Where(n => n % 3 == 0 && n % 5 == 0)
                              .ToArray();
        Console.WriteLine("3の倍数かつ5の倍数の数(PLINQ):");
        foreach (var num in filtered)
        {
            Console.WriteLine(num);
        }
    }
}
3の倍数かつ5の倍数の数(PLINQ):
15
30
45
60
75
90
105
120
135
150
165
180
195
210
225
240
255
270
285
300
315
330
345
360
375
390
405
420
435
450
465
480
495
510
525
540
555
570
585
600
615
630
645
660
675
690
705
720
735
750
765
780
795
810
825
840
855
870
885
900
915
930
945
960
975
990

PLINQは内部でデータを分割し、複数スレッドで並列に処理するため、CPUリソースを有効活用できます。

ただし、並列処理のオーバーヘッドがあるため、データ量が少ない場合や処理が軽い場合は逆に遅くなることもあります。

また、PLINQのクエリは順序を保証しません。

順序を維持したい場合はAsOrdered()を追加しますが、パフォーマンスに影響が出ることがあります。

IAsyncEnumerable<T>とawait foreachでの非同期抽出

.NET Core 3.0以降では、非同期ストリームを扱うためにIAsyncEnumerable<T>インターフェースが導入されました。

これにより、非同期にデータを逐次取得しながら処理できるようになり、特にI/O待ちが発生するシナリオで有効です。

IAsyncEnumerable<T>await foreach構文で列挙でき、非同期に要素を取得しながら処理を進められます。

LINQのWhereなどの拡張メソッドは標準では非同期対応していませんが、System.Linq.Asyncパッケージを使うと非同期LINQメソッドが利用可能です。

以下のサンプルは、非同期にデータを生成し、非同期LINQのWhereで条件抽出を行い、await foreachで結果を列挙しています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using System.Linq.Async; // NuGetパッケージ System.Linq.Async が必要
class Program
{
    static async IAsyncEnumerable<int> GenerateNumbersAsync()
    {
        for (int i = 1; i <= 20; i++)
        {
            await Task.Delay(50); // 非同期処理のシミュレーション
            yield return i;
        }
    }
    static async Task Main()
    {
        // 非同期ストリームから偶数だけを抽出
        var filtered = GenerateNumbersAsync().Where(n => n % 2 == 0);
        Console.WriteLine("非同期ストリームから偶数を抽出:");
        await foreach (var num in filtered)
        {
            Console.WriteLine(num);
        }
    }
}
非同期ストリームから偶数を抽出:
2
4
6
8
10
12
14
16
18
20

この例では、GenerateNumbersAsyncメソッドが非同期に数値を生成し、Whereで偶数だけを抽出しています。

await foreachで非同期に結果を受け取りながら順次表示しています。

IAsyncEnumerable<T>はデータの逐次取得が必要なAPI呼び出しやファイル読み込み、ネットワーク通信などの非同期処理に適しており、UIの応答性を保ちながら大量データを処理できます。

PLINQとIAsyncEnumerable<T>はそれぞれ並列処理と非同期処理の手法として強力です。

用途やシナリオに応じて使い分けることで、C#のデータ抽出処理をより効率的かつ柔軟に実装できます。

よくある落とし穴と対策

期待しない実行順序の回避

LINQのWhereメソッドをはじめとする多くのLINQメソッドは遅延実行を採用しているため、クエリの実行タイミングや順序に注意が必要です。

特に、複数のクエリを連結したり、同じクエリを複数回列挙したりすると、期待しない順序や副作用が発生することがあります。

例えば、以下のコードではWhereの条件に副作用のあるメソッドを使っているため、列挙のたびに副作用が発生し、実行順序が予測しにくくなります。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static int counter = 0;
    static bool Condition(int x)
    {
        counter++;
        Console.WriteLine($"Condition called {counter} times for value {x}");
        return x % 2 == 0;
    }
    static void Main()
    {
        var numbers = new List<int> { 1, 2, 3, 4, 5 };
        var query = numbers.Where(Condition);
        Console.WriteLine("1回目の列挙:");
        foreach (var num in query)
        {
            Console.WriteLine(num);
        }
        Console.WriteLine("2回目の列挙:");
        foreach (var num in query)
        {
            Console.WriteLine(num);
        }
    }
}
Condition called 1 times for value 1
Condition called 2 times for value 2
2
Condition called 3 times for value 3
Condition called 4 times for value 4
4
Condition called 5 times for value 5
1回目の列挙:
2
4
Condition called 6 times for value 1
Condition called 7 times for value 2
2
Condition called 8 times for value 3
Condition called 9 times for value 4
4
Condition called 10 times for value 5
2回目の列挙:
2
4

このように、queryを2回列挙するとConditionが2回ずつ呼ばれ、処理が重複しています。

副作用のある処理は予期せぬ動作やパフォーマンス低下の原因になるため注意が必要です。

対策としては、クエリの結果を一度ToList()ToArray()で即時実行し、結果をキャッシュしてから複数回利用する方法があります。

var cached = query.ToList();
foreach (var num in cached) { /* 処理 */ }
foreach (var num in cached) { /* 処理 */ }

これにより、条件判定は一度だけ行われ、実行順序や副作用の問題を回避できます。

例外発生時のクリーンアップ手法

LINQのクエリ実行中に例外が発生すると、処理が中断されるだけでなく、リソースの解放や後処理が適切に行われないことがあります。

特に、ファイルやデータベース接続などの外部リソースを扱う場合は、例外発生時のクリーンアップが重要です。

例えば、IEnumerable<T>を返すメソッド内でファイルを読み込みつつLINQでフィルタリングする場合、例外が発生するとファイルが閉じられない恐れがあります。

対策としては、using文やtry-finallyブロックでリソースを確実に解放することが基本です。

また、LINQの遅延実行を考慮し、クエリの実行(列挙)をusingのスコープ内で行うことが重要です。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
class Program
{
    static IEnumerable<string> ReadLines(string path)
    {
        using var reader = new StreamReader(path);
        string line;
        while ((line = reader.ReadLine()) != null)
        {
            yield return line;
        }
    }
    static void Main()
    {
        var path = "sample.txt";
        try
        {
            // クエリの実行はusingスコープ内で行う
            var lines = ReadLines(path).Where(line => line.Contains("keyword"));
            foreach (var line in lines)
            {
                Console.WriteLine(line);
            }
        }
        catch (IOException ex)
        {
            Console.WriteLine($"ファイル読み込みエラー: {ex.Message}");
        }
    }
}

この例では、ReadLinesメソッド内でusingを使いファイルを開閉していますが、遅延実行のためファイルはforeachの列挙時に読み込まれます。

したがって、foreachusingのスコープ内で実行される必要があります。

また、例外発生時にリソースを確実に解放するため、try-catchでエラーハンドリングを行い、必要に応じてログ出力やリトライ処理を実装しましょう。

可読性を保つラムダ式の分割

LINQの条件式はラムダ式で記述しますが、複雑な条件を一つのラムダ式に詰め込むと可読性が低下し、バグの温床になることがあります。

特に複数の論理演算子やネストした条件がある場合は、適切に分割してコードを整理することが重要です。

例えば、以下のように複雑な条件を一行で書くと読みにくくなります。

var filtered = items.Where(x => x.IsActive && (x.Score > 50 || x.Level >= 10) && x.Name.StartsWith("A"));

これを分割して可読性を高める方法の一つは、条件をローカル関数やメソッドに切り出すことです。

bool IsValid(Item x)
{
    return x.IsActive && (x.Score > 50 || x.Level >= 10) && x.Name.StartsWith("A");
}
var filtered = items.Where(IsValid);

または、ラムダ式内で複数行に分けて書くことも可能です。

var filtered = items.Where(x =>
{
    bool condition1 = x.IsActive;
    bool condition2 = x.Score > 50 || x.Level >= 10;
    bool condition3 = x.Name.StartsWith("A");
    return condition1 && condition2 && condition3;
});

このように分割することで、各条件の意味が明確になり、将来的な修正やデバッグが容易になります。

さらに、条件式が複数箇所で使われる場合は、外部メソッドやFunc<T,bool>デリゲートとして再利用可能にしておくと保守性が向上します。

これらのポイントを意識してLINQを使うことで、予期せぬ動作や例外のトラブルを防ぎ、読みやすく保守しやすいコードを書くことができます。

実務サンプルケース

JSONデータのフィルタリング

実務では、APIから取得したJSONデータをC#のオブジェクトにデシリアライズし、LINQで条件抽出するケースが多くあります。

System.Text.JsonNewtonsoft.Jsonなどのライブラリを使い、JSONをクラスに変換した後、Whereメソッドでフィルタリングを行います。

以下は、Newtonsoft.Jsonを使ってJSON文字列をデシリアライズし、特定の条件でフィルタリングする例です。

using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
class Product
{
    public string Name { get; set; }
    public int Price { get; set; }
    public bool InStock { get; set; }
}
class Program
{
    static void Main()
    {
        string json = @"
        [
            { 'Name': 'ペン', 'Price': 150, 'InStock': true },
            { 'Name': 'ノート', 'Price': 300, 'InStock': false },
            { 'Name': '消しゴム', 'Price': 100, 'InStock': true }
        ]";
        var products = JsonConvert.DeserializeObject<List<Product>>(json);
        // 在庫ありかつ価格が200円以下の商品を抽出
        var filtered = products.Where(p => p.InStock && p.Price <= 200);
        Console.WriteLine("在庫ありかつ価格200円以下の商品:");
        foreach (var p in filtered)
        {
            Console.WriteLine($"{p.Name} - {p.Price}円");
        }
    }
}
在庫ありかつ価格200円以下の商品:
ペン - 150円
消しゴム - 100円

このように、JSONデータをオブジェクト化してからLINQで条件抽出することで、柔軟かつ直感的にデータ操作が可能です。

データベース取得結果への二次絞り込み

ORM(Object-Relational Mapping)を使ってデータベースから取得した結果に対して、さらにLINQで二次的な絞り込みを行うこともよくあります。

Entity Frameworkなどでは、クエリは遅延実行されるため、必要な条件を追加してから実行するのが効率的です。

以下は、Entity Framework Coreでデータベースからユーザーを取得し、メモリ上でさらに条件を追加して絞り込む例です。

using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsActive { get; set; }
}
class AppDbContext : DbContext
{
    public DbSet<User> Users { get; set; }
    // DbContextの設定は省略
}
class Program
{
    static void Main()
    {
        using var context = new AppDbContext();
        // データベースからアクティブユーザーを取得(SQLで絞り込み)
        var activeUsersQuery = context.Users.Where(u => u.IsActive);
        // メモリ上で名前に「田中」を含むユーザーに絞り込み
        var filteredUsers = activeUsersQuery.AsEnumerable()
                                            .Where(u => u.Name.Contains("田中"));
        foreach (var user in filteredUsers)
        {
            Console.WriteLine($"{user.Id}: {user.Name}");
        }
    }
}

この例では、Where句でまずデータベース側でIsActivetrueのユーザーを絞り込み、AsEnumerable()でクエリを実行してメモリ上に取得した後、さらに名前に「田中」を含むユーザーをLINQで絞り込んでいます。

パフォーマンスを考慮すると、可能な限りSQL側で条件を指定するのが望ましいですが、複雑な条件やメモリ上での処理が必要な場合は二次絞り込みが有効です。

CSV読み込み後の条件抽出

CSVファイルを読み込んでデータを解析し、LINQで条件抽出するケースも多いです。

System.IOFile.ReadLinesFile.ReadAllLinesでCSVを読み込み、Splitで分割してオブジェクト化し、Whereで絞り込みを行います。

以下は、簡単なCSVファイルを読み込み、価格が100円以上の商品を抽出する例です。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
class Product
{
    public string Name { get; set; }
    public int Price { get; set; }
}
class Program
{
    static void Main()
    {
        string path = "products.csv";
        // CSV例:
        // ペン,150
        // ノート,80
        // 消しゴム,120
        var products = File.ReadLines(path)
            .Select(line =>
            {
                var parts = line.Split(',');
                return new Product
                {
                    Name = parts[0],
                    Price = int.Parse(parts[1])
                };
            });
        var filtered = products.Where(p => p.Price >= 100);
        Console.WriteLine("価格100円以上の商品:");
        foreach (var p in filtered)
        {
            Console.WriteLine($"{p.Name} - {p.Price}円");
        }
    }
}
価格100円以上の商品:
ペン - 150円
消しゴム - 120円

この方法は小規模なCSVファイルに適しており、より大規模なファイルや複雑なCSVには専用ライブラリ(CsvHelperなど)の利用を検討してください。

条件式の簡潔さと保守性のバランス

実務でのLINQ条件式は、簡潔に書くことと保守性を両立させることが重要です。

複雑な条件を一行で書きすぎると可読性が落ち、将来的な修正やバグの原因になります。

一方で、冗長すぎるとコードが長くなりすぎて管理が難しくなります。

以下のポイントを意識するとよいでしょう。

  • 条件をメソッドやローカル関数に切り出す

複数の場所で使う条件や複雑な条件は名前付きメソッドにまとめると再利用しやすくなります。

  • 匿名型や変数で中間結果を保持する

複雑な条件の一部を変数に分割して意味を明確にすることで、読みやすくなります。

  • コメントで条件の意図を補足する

特にビジネスロジックに関わる条件は、なぜその条件が必要かをコメントで説明すると保守性が向上します。

bool IsEligible(Product p)
{
    // 在庫ありかつ価格が適正範囲内の商品
    return p.InStock && p.Price >= 100 && p.Price <= 1000;
}
var filtered = products.Where(IsEligible);

このように、条件式の簡潔さと保守性のバランスを取りながらコードを書くことが実務では求められます。

パフォーマンス対策の優先順位

実務でLINQを使う際のパフォーマンス対策は、以下の優先順位で検討すると効率的です。

  1. SQLやAPIなどデータ取得元での絞り込み

可能な限りデータベースやAPIのクエリで条件を指定し、不要なデータを取得しない。

  1. 遅延実行の特性を活かす

必要なタイミングでクエリを実行し、不要な処理を避けます。

  1. クエリの合成と一括評価

複数の条件をまとめて一度に評価し、中間コレクションの生成を減らす。

  1. 不要なToList()ToArray()の多用を避ける

メモリ消費や処理時間の無駄を防ぐ。

  1. 必要に応じて並列処理(PLINQ)を検討

大量データやCPU負荷の高い処理に対して効果的。

  1. プロファイリングとボトルネックの特定

実際のパフォーマンスを測定し、問題箇所を特定して最適化。

これらを踏まえ、まずは正確で読みやすいコードを書くことを優先し、必要に応じてパフォーマンス改善を行うのが実務でのベストプラクティスです。

まとめ

この記事では、C#のLINQを使った多彩なデータ抽出方法を解説しました。

基本的なWhereメソッドの使い方から、複数条件の組み合わせ、インデックス利用、Null安全対策、文字列や日付の特化処理、複合オブジェクトの条件抽出まで幅広く紹介しています。

さらに、パフォーマンス最適化や動的条件生成、並列・非同期処理、実務での具体例やよくある落とし穴の対策も網羅。

これにより、LINQを効果的かつ安全に活用し、保守性と効率性を両立したコードを書くための知識が身につきます。

関連記事

Back to top button
目次へ