LINQ

【C#】LINQで簡単かつ高速にデータフィルタリングを行う方法と実用サンプル集

LINQなら配列やリストをWhereで簡潔に絞り込めます。

クエリ構文とメソッド構文が選べ、条件はラムダ式に論理演算子を重ねるだけ。

OrderByなどと連結すれば抽出から並べ替えまで一気に記述でき、読みやすさと保守性が高まります。

LINQフィルタリングの基礎

LINQ(Language Integrated Query)は、C#でデータコレクションを扱う際に非常に便利な機能です。

特にデータのフィルタリングにおいては、Whereメソッドやwhere句を使うことで、条件に合致する要素だけを簡単に抽出できます。

ここでは、LINQのフィルタリングの基本的な使い方を詳しく解説いたします。

Whereメソッドの役割

Whereメソッドは、LINQの中でも最もよく使われるフィルタリング用のメソッドです。

コレクションの中から、指定した条件を満たす要素だけを抽出して返します。

条件はラムダ式で記述し、真偽値を返す関数を渡します。

Whereメソッドは遅延実行されるため、実際に結果を使うまで処理は行われません。

値型コレクションでの最小例

まずは、整数の配列から偶数だけを抽出するシンプルな例を見てみましょう。

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

このコードでは、numbers配列の中からn % 2 == 0(2で割り切れる)という条件を満たす要素だけを抽出しています。

Whereメソッドに渡したラムダ式が条件を表しており、これに合致する要素だけがevenNumbersに含まれます。

オブジェクトコレクションでのプロパティ条件

次に、クラスのオブジェクトが入ったコレクションから特定のプロパティの値を条件にフィルタリングする例を紹介します。

ここでは、Personクラスのリストから年齢が20歳以上の人だけを抽出します。

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 = "Alice", Age = 18 },
            new Person { Name = "Bob", Age = 25 },
            new Person { Name = "Charlie", Age = 17 },
            new Person { Name = "Diana", Age = 30 }
        };
        // 年齢が20歳以上の人を抽出
        var adults = people.Where(p => p.Age >= 20);
        Console.WriteLine("20歳以上の人:");
        foreach (var person in adults)
        {
            Console.WriteLine($"{person.Name} ({person.Age}歳)");
        }
    }
}
20歳以上の人:
Bob (25歳)
Diana (30歳)

この例では、PersonオブジェクトのAgeプロパティを条件にしています。

Whereメソッドのラムダ式でp.Age >= 20と指定することで、20歳以上の人だけを抽出しています。

オブジェクトのプロパティを使ったフィルタリングは、実務でも非常に多く使われるパターンです。

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

LINQには大きく分けて2つの記述方法があります。

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

どちらも同じ処理を実現できますが、書き方や可読性に違いがあります。

記述量と可読性

クエリ構文はSQLに似ているため、SQLに慣れている方には直感的に理解しやすいです。

特に複雑なクエリを記述する際に見通しが良くなることがあります。

一方、メソッド構文はラムダ式を使うため、C#の関数型プログラミング的な書き方が好きな方に向いています。

簡単なフィルタリングであれば、メソッド構文の方が短く書けることが多いです。

逆に複数の操作を組み合わせる場合は、クエリ構文の方が読みやすいケースもあります。

コード例で見る違い

同じ条件でフィルタリングを行う例を、クエリ構文とメソッド構文で比較してみます。

ここでは、文字列配列から長さが3の単語だけを抽出します。

クエリ構文の例:

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        string[] words = { "the", "quick", "brown", "fox", "jumps" };
        var query = from word in words
                    where word.Length == 3
                    select word;
        Console.WriteLine("長さが3の単語:");
        foreach (var w in query)
        {
            Console.WriteLine(w);
        }
    }
}
長さが3の単語:
the
fox

メソッド構文の例:

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        string[] words = { "the", "quick", "brown", "fox", "jumps" };
        var query = words.Where(word => word.Length == 3);
        Console.WriteLine("長さが3の単語:");
        foreach (var w in query)
        {
            Console.WriteLine(w);
        }
    }
}
長さが3の単語:
the
fox

どちらの書き方も同じ結果を返します。

クエリ構文はfromwhereselectのキーワードを使い、SQLに近い形で記述します。

メソッド構文はWhereメソッドにラムダ式を渡す形で書きます。

用途や好みによって使い分けると良いでしょう。

なお、複雑なクエリや複数の操作を組み合わせる場合は、クエリ構文の方が読みやすくなることがありますが、メソッド構文の方が柔軟に拡張しやすいというメリットもあります。

条件指定のバリエーション

複数条件の組み合わせ

AND条件

複数の条件をすべて満たす要素を抽出したい場合は、論理積(AND)を表す&&演算子を使います。

例えば、年齢が20歳以上かつ名前が”Alice”の人を抽出する場合は以下のように記述します。

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 = "Alice", Age = 25 },
            new Person { Name = "Bob", Age = 30 },
            new Person { Name = "Alice", Age = 18 },
            new Person { Name = "Charlie", Age = 22 }
        };
        var filtered = people.Where(p => p.Age >= 20 && p.Name == "Alice");
        Console.WriteLine("年齢が20歳以上かつ名前がAliceの人:");
        foreach (var person in filtered)
        {
            Console.WriteLine($"{person.Name} ({person.Age}歳)");
        }
    }
}
年齢が20歳以上かつ名前がAliceの人:
Alice (25歳)

この例では、p.Age >= 20 && p.Name == "Alice"という条件で両方を満たす要素だけを抽出しています。

AND条件は複数の条件を厳密に満たす必要がある場合に使います。

OR条件

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

例えば、年齢が18歳未満または30歳以上の人を抽出する場合は以下のように書きます。

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 = "Alice", Age = 17 },
            new Person { Name = "Bob", Age = 25 },
            new Person { Name = "Charlie", Age = 30 },
            new Person { Name = "Diana", Age = 15 }
        };
        var filtered = people.Where(p => p.Age < 18 || p.Age >= 30);
        Console.WriteLine("年齢が18歳未満または30歳以上の人:");
        foreach (var person in filtered)
        {
            Console.WriteLine($"{person.Name} ({person.Age}歳)");
        }
    }
}
年齢が18歳未満または30歳以上の人:
Alice (17歳)
Charlie (30歳)
Diana (15歳)

このように、||を使うことでどちらかの条件を満たす要素を抽出できます。

OR条件は幅広い条件で抽出したい場合に便利です。

否定条件

条件を満たさない要素を抽出したい場合は、論理否定演算子!を使います。

例えば、名前が”Alice”でない人を抽出する場合は以下のように書きます。

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 = "Alice", Age = 25 },
            new Person { Name = "Bob", Age = 30 },
            new Person { Name = "Charlie", Age = 22 }
        };
        var filtered = people.Where(p => p.Name != "Alice");
        Console.WriteLine("名前がAliceでない人:");
        foreach (var person in filtered)
        {
            Console.WriteLine($"{person.Name} ({person.Age}歳)");
        }
    }
}
名前がAliceでない人:
Bob (30歳)
Charlie (22歳)

または!を使って否定することも可能です。

var filtered = people.Where(p => !(p.Name == "Alice"));

どちらの書き方でも同じ結果になります。

否定条件は特定の条件を除外したい場合に使います。

比較演算子の応用

文字列比較

文字列の比較は==!=で行えますが、大文字・小文字を区別したくない場合はstring.Equalsメソッドを使い、StringComparisonオプションを指定すると便利です。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        string[] fruits = { "Apple", "banana", "APPLE", "Banana", "Cherry" };
        // 大文字小文字を区別せずに"apple"を抽出
        var filtered = fruits.Where(f => string.Equals(f, "apple", StringComparison.OrdinalIgnoreCase));
        Console.WriteLine("大文字小文字を区別せずに'apple'を抽出:");
        foreach (var fruit in filtered)
        {
            Console.WriteLine(fruit);
        }
    }
}
大文字小文字を区別せずに'apple'を抽出:
Apple
APPLE

このようにstring.EqualsStringComparison.OrdinalIgnoreCaseを指定することで、大文字・小文字を無視した比較ができます。

数値レンジ

数値の範囲でフィルタリングする場合は、比較演算子を組み合わせて範囲指定を行います。

例えば、10以上20以下の数値を抽出する場合は以下のように書きます。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 5, 10, 15, 20, 25 };
        var filtered = numbers.Where(n => n >= 10 && n <= 20);
        Console.WriteLine("10以上20以下の数値:");
        foreach (var num in filtered)
        {
            Console.WriteLine(num);
        }
    }
}
10以上20以下の数値:
10
15
20

範囲指定は数値だけでなく、日付やその他の比較可能な型でも同様に使えます。

Null許容型の扱い

Nullable<T>型(int?DateTime?など)を扱う場合は、HasValueプロパティでnullチェックを行い、値が存在する場合のみ比較を行うのが安全です。

using System;
using System.Collections.Generic;
using System.Linq;
class Item
{
    public string Name { get; set; }
    public int? Quantity { get; set; }
}
class Program
{
    static void Main()
    {
        var items = new List<Item>
        {
            new Item { Name = "Pen", Quantity = 10 },
            new Item { Name = "Notebook", Quantity = null },
            new Item { Name = "Eraser", Quantity = 5 }
        };
        // Quantityがnullでなく、5以上のものを抽出
        var filtered = items.Where(i => i.Quantity.HasValue && i.Quantity >= 5);
        Console.WriteLine("数量が5以上のアイテム:");
        foreach (var item in filtered)
        {
            Console.WriteLine($"{item.Name} (数量: {item.Quantity})");
        }
    }
}
数量が5以上のアイテム:
Pen (数量: 10)
Eraser (数量: 5)

nullチェックをしないと、Quantity >= 5の比較で例外が発生する可能性があるため注意が必要です。

入れ子データのフィルタリング

AnyとAllによる子要素条件

コレクションの中にさらにコレクションがある場合、子要素の条件で親要素をフィルタリングすることがよくあります。

Anyメソッドは子要素のうち1つでも条件を満たすかを判定し、Allメソッドはすべての子要素が条件を満たすかを判定します。

以下は、Orderクラスの中に複数のItemがあり、注文の中に価格が1000円以上のアイテムが1つでもある注文だけを抽出する例です。

using System;
using System.Collections.Generic;
using System.Linq;
class Item
{
    public string Name { get; set; }
    public int Price { get; set; }
}
class Order
{
    public int OrderId { get; set; }
    public List<Item> Items { get; set; }
}
class Program
{
    static void Main()
    {
        var orders = new List<Order>
        {
            new Order
            {
                OrderId = 1,
                Items = new List<Item>
                {
                    new Item { Name = "Pen", Price = 500 },
                    new Item { Name = "Notebook", Price = 1500 }
                }
            },
            new Order
            {
                OrderId = 2,
                Items = new List<Item>
                {
                    new Item { Name = "Eraser", Price = 300 },
                    new Item { Name = "Pencil", Price = 200 }
                }
            }
        };
        // 価格が1000円以上のアイテムが1つでもある注文を抽出
        var filtered = orders.Where(o => o.Items.Any(i => i.Price >= 1000));
        Console.WriteLine("価格が1000円以上のアイテムが含まれる注文:");
        foreach (var order in filtered)
        {
            Console.WriteLine($"注文ID: {order.OrderId}");
        }
    }
}
価格が1000円以上のアイテムが含まれる注文:
注文ID: 1

逆に、すべてのアイテムが価格500円以上である注文だけを抽出する場合はAllを使います。

var filteredAll = orders.Where(o => o.Items.All(i => i.Price >= 500));

SelectManyで平坦化してからの抽出

入れ子のコレクションを一旦平坦化(フラット化)してからフィルタリングする方法もあります。

SelectManyメソッドを使うと、複数の子コレクションを1つのシーケンスにまとめられます。

例えば、すべての注文の中から価格が1000円以上のアイテムだけを抽出したい場合は以下のように書きます。

using System;
using System.Collections.Generic;
using System.Linq;
class Item
{
    public string Name { get; set; }
    public int Price { get; set; }
}
class Order
{
    public int OrderId { get; set; }
    public List<Item> Items { get; set; }
}
class Program
{
    static void Main()
    {
        var orders = new List<Order>
        {
            new Order
            {
                OrderId = 1,
                Items = new List<Item>
                {
                    new Item { Name = "Pen", Price = 500 },
                    new Item { Name = "Notebook", Price = 1500 }
                }
            },
            new Order
            {
                OrderId = 2,
                Items = new List<Item>
                {
                    new Item { Name = "Eraser", Price = 300 },
                    new Item { Name = "Pencil", Price = 200 }
                }
            }
        };
        // すべての注文の中から価格が1000円以上のアイテムを抽出
        var expensiveItems = orders
            .SelectMany(o => o.Items)
            .Where(i => i.Price >= 1000);
        Console.WriteLine("価格が1000円以上のアイテム一覧:");
        foreach (var item in expensiveItems)
        {
            Console.WriteLine($"{item.Name} ({item.Price}円)");
        }
    }
}
価格が1000円以上のアイテム一覧:
Notebook (1500円)

SelectManyで子コレクションを1つにまとめてからWhereで条件を指定することで、入れ子構造を意識せずにフィルタリングできます。

複雑なネスト構造のデータを扱う際に便利なテクニックです。

パフォーマンスを意識した絞り込み

遅延実行と即時実行

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

つまり、クエリを定義しただけでは処理は実行されず、結果を実際に使うタイミングで初めて処理が走ります。

これにより、無駄な処理を避け効率的にデータを扱えますが、場合によっては意図しないタイミングで処理が複数回走ることもあります。

ToList/ToArrayを呼ぶタイミング

ToList()ToArray()は即時実行(Immediate Execution)を引き起こすメソッドです。

これらを呼ぶと、クエリの結果がすぐに評価され、メモリ上にリストや配列として格納されます。

例えば、以下のコードではWhereでフィルタリングした結果をToList()で即時実行しています。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = Enumerable.Range(1, 10);
        // 遅延実行のクエリ定義
        var query = numbers.Where(n => n % 2 == 0);
        // ここではまだ処理は実行されていない
        // 即時実行:結果をリストに格納
        var evenNumbers = query.ToList();
        Console.WriteLine("偶数の一覧:");
        foreach (var num in evenNumbers)
        {
            Console.WriteLine(num);
        }
    }
}
偶数の一覧:
2
4
6
8
10

ToList()を呼ぶことで、queryの結果が一度に評価されます。

これにより、以降の処理で何度もクエリが実行されることを防げます。

一方、ToList()を呼ばずにforeachで直接列挙すると、列挙のたびにクエリが実行される可能性があります。

大量データや重い処理が含まれる場合は、ToList()ToArray()で一度評価しておくとパフォーマンスが安定します。

ただし、メモリ消費が増えるため、必要な範囲だけ評価することが重要です。

例えば、ページング処理でSkipTakeを使う場合は、ToList()は最後に呼び出すのが基本です。

インメモリコレクションとIQueryable

LINQは主に2種類のデータソースに対して使われます。

1つはメモリ上のコレクションIEnumerable<T>、もう1つはデータベースなどの外部データソースIQueryable<T>です。

これらはクエリの実行方法やパフォーマンスに大きな違いがあります。

IEnumerable<T>はメモリ内のデータに対してLINQを適用し、C#のコードとして処理が実行されます。

一方、IQueryable<T>はLINQクエリをデータベースのクエリ言語(SQLなど)に変換し、データベース側で効率的に処理を行います。

データベースクエリへの変換最適化

IQueryable<T>を使う場合、LINQのクエリはExpression Treeとして解析され、SQLなどのクエリに変換されます。

これにより、必要なデータだけをデータベースから取得でき、ネットワークやメモリの負荷を減らせます。

例えば、Entity Frameworkを使った以下の例では、Where句の条件がSQLのWHERE句に変換されます。

using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}
class MyDbContext : DbContext
{
    public DbSet<Person> People { get; set; }
}
class Program
{
    static void Main()
    {
        using var context = new MyDbContext();
        // IQueryable<Person>としてクエリを定義
        var query = context.People.Where(p => p.Age >= 20);
        // SQLクエリはここで実行される
        var adults = query.ToList();
        foreach (var person in adults)
        {
            Console.WriteLine($"{person.Name} ({person.Age}歳)");
        }
    }
}
Microsoft.EntityFrameworkCoreのインストール

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

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

dotnet add package Microsoft.EntityFrameworkCore

この場合、Where(p => p.Age >= 20)の条件はSQLのWHERE Age >= 20に変換され、データベース側で絞り込みが行われます。

これにより、不要なデータをアプリケーションに読み込まずに済みます。

ただし、IQueryableのクエリに対して、ToList()ToArray()を呼ぶ前に複雑なC#のメソッドや関数を使うと、SQLに変換できずに全件取得してからメモリ上で処理されることがあります。

これがパフォーマンス低下の原因になるため注意が必要です。

実行計画の確認ポイント

データベースに対してLINQクエリを発行する場合、生成されるSQLの実行計画を確認することがパフォーマンス改善に役立ちます。

実行計画は、SQL Server Management Studio(SSMS)や他のデータベース管理ツールで確認可能です。

実行計画で注目すべきポイントは以下の通りです。

ポイント内容
インデックスの利用クエリが適切なインデックスを使っているか。インデックススキャンやインデックスシークの違いに注目。
フルテーブルスキャンテーブル全体をスキャンしている場合、パフォーマンスが低下する可能性が高いでしょう。
ジョインの種類ネストループジョインやマージジョインなど、効率的な結合が行われているか。
フィルタリングの順序WHERE句の条件が早期に適用されているか。不要なデータを早く絞り込めているか。
クエリの複雑さ複雑すぎるクエリは分割や最適化が必要な場合があります。

LINQで書いたクエリが期待通りのSQLに変換されているか、また実行計画が効率的かを確認し、必要に応じてクエリの書き方を見直すことが重要です。

また、Entity FrameworkなどのORMでは、ToQueryString()メソッドを使って生成されるSQLを確認できます。

var sql = context.People.Where(p => p.Age >= 20).ToQueryString();
Console.WriteLine(sql);

これにより、どのようなSQLが発行されているかを把握しやすくなります。

SQLの内容を理解し、実行計画と照らし合わせてパフォーマンスチューニングを行いましょう。

よく使う組み合わせメソッド

OrderByとの連携でソート付き抽出

Whereメソッドで条件に合致する要素を抽出した後、OrderByメソッドを使って結果をソートすることがよくあります。

OrderByは指定したキーに基づいて昇順に並べ替えを行い、OrderByDescendingを使うと降順にソートできます。

以下は、年齢が20歳以上の人を抽出し、年齢の昇順で並べ替える例です。

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 = "Alice", Age = 25 },
            new Person { Name = "Bob", Age = 20 },
            new Person { Name = "Charlie", Age = 30 },
            new Person { Name = "Diana", Age = 22 }
        };
        var filteredAndSorted = people
            .Where(p => p.Age >= 20)
            .OrderBy(p => p.Age);
        Console.WriteLine("20歳以上の人を年齢昇順で表示:");
        foreach (var person in filteredAndSorted)
        {
            Console.WriteLine($"{person.Name} ({person.Age}歳)");
        }
    }
}
20歳以上の人を年齢昇順で表示:
Bob (20歳)
Diana (22歳)
Alice (25歳)
Charlie (30歳)

このように、Whereで絞り込みを行い、その後OrderByでソートすることで、条件に合致したデータを見やすく整理できます。

Selectで加工しながらの絞り込み

Selectメソッドは、コレクションの各要素を別の形に変換するために使います。

Whereで絞り込みを行った後にSelectを使うことで、必要なプロパティだけを抽出したり、加工した結果を取得できます。

以下は、年齢が20歳以上の人の名前だけを抽出する例です。

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 = "Alice", Age = 25 },
            new Person { Name = "Bob", Age = 18 },
            new Person { Name = "Charlie", Age = 30 },
            new Person { Name = "Diana", Age = 22 }
        };
        var names = people
            .Where(p => p.Age >= 20)
            .Select(p => p.Name);
        Console.WriteLine("20歳以上の人の名前:");
        foreach (var name in names)
        {
            Console.WriteLine(name);
        }
    }
}
20歳以上の人の名前:
Alice
Charlie
Diana

Selectを使うことで、必要な情報だけを取り出し、メモリ使用量を抑えたり、UI表示用にデータを整形したりできます。

GroupBy前後のフィルタリング

GroupByはコレクションの要素を指定したキーでグループ化します。

グループ化の前後でWhereを使ってフィルタリングすることが可能です。

グループ化前のフィルタリング

グループ化する前に条件を絞り込むことで、対象データを限定してからグループ化できます。

using System;
using System.Collections.Generic;
using System.Linq;
class Person
{
    public string Department { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}
class Program
{
    static void Main()
    {
        var employees = new List<Person>
        {
            new Person { Department = "Sales", Name = "Alice", Age = 25 },
            new Person { Department = "Sales", Name = "Bob", Age = 30 },
            new Person { Department = "HR", Name = "Charlie", Age = 28 },
            new Person { Department = "HR", Name = "Diana", Age = 22 }
        };
        var filteredGroups = employees
            .Where(e => e.Age >= 25)
            .GroupBy(e => e.Department);
        Console.WriteLine("25歳以上の社員を部署ごとにグループ化:");
        foreach (var group in filteredGroups)
        {
            Console.WriteLine($"部署: {group.Key}");
            foreach (var employee in group)
            {
                Console.WriteLine($"  {employee.Name} ({employee.Age}歳)");
            }
        }
    }
}
25歳以上の社員を部署ごとにグループ化:
部署: Sales
  Alice (25歳)
  Bob (30歳)
部署: HR
  Charlie (28歳)

グループ化後のフィルタリング

グループ化した後に、グループの条件で絞り込むこともできます。

例えば、グループ内の人数が2人以上の部署だけを抽出する場合です。

var largeGroups = employees
    .GroupBy(e => e.Department)
    .Where(g => g.Count() >= 2);

このように、グループのサイズやグループ内の条件を使ってフィルタリングできます。

Aggregateと条件絞り込み

Aggregateメソッドは、コレクションの要素を1つの値に集約するために使います。

条件絞り込みと組み合わせることで、特定の条件を満たす要素だけを対象に集約処理が可能です。

例えば、年齢が20歳以上の人の合計年齢を計算する例です。

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 = "Alice", Age = 25 },
            new Person { Name = "Bob", Age = 18 },
            new Person { Name = "Charlie", Age = 30 },
            new Person { Name = "Diana", Age = 22 }
        };
        var totalAge = people
            .Where(p => p.Age >= 20)
            .Aggregate(0, (sum, p) => sum + p.Age);
        Console.WriteLine($"20歳以上の人の合計年齢: {totalAge}");
    }
}
20歳以上の人の合計年齢: 77

Aggregateの第1引数は初期値、第2引数は集約処理の関数です。

ここでは、条件に合う人の年齢を順に足し合わせています。

Aggregateは合計以外にも、最大値・最小値の計算や文字列の連結など、さまざまな集約処理に応用できます。

条件絞り込みと組み合わせて使うことで、柔軟な集計が可能です。

コレクション種類別の注意点

ListとArray

List<T>ArrayはC#で最もよく使われるコレクションですが、LINQでの扱い方やパフォーマンスに若干の違いがあります。

  • Array(配列)

配列は固定長で、要素数の変更ができません。

LINQのメソッドは配列に対しても問題なく使えます。

配列はメモリ上で連続しているため、アクセス速度が速いのが特徴です。

ただし、要素の追加や削除ができないため、動的なデータ操作には向いていません。

  • List<T>

List<T>は可変長のコレクションで、要素の追加・削除が容易です。

LINQのメソッドはList<T>に対しても同様に使えます。

内部的には配列を使っていますが、サイズ変更時に新しい配列を作成するため、頻繁なサイズ変更はパフォーマンスに影響を与えることがあります。

LINQのフィルタリングにおいては、WhereSelectなどのメソッドはどちらのコレクションでも同じように動作します。

違いは主にコレクションの性質や用途にあります。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] array = { 1, 2, 3, 4, 5 };
        List<int> list = new List<int> { 1, 2, 3, 4, 5 };
        var evenFromArray = array.Where(n => n % 2 == 0);
        var evenFromList = list.Where(n => n % 2 == 0);
        Console.WriteLine("配列から偶数:");
        foreach (var n in evenFromArray)
        {
            Console.WriteLine(n);
        }
        Console.WriteLine("リストから偶数:");
        foreach (var n in evenFromList)
        {
            Console.WriteLine(n);
        }
    }
}
配列から偶数:
2
4
リストから偶数:
2
4

Dictionaryでキー/値を条件に

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

LINQでフィルタリングする際は、KeyValueプロパティを使って条件を指定します。

例えば、値が100以上の要素だけを抽出する場合は以下のように書きます。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var dict = new Dictionary<string, int>
        {
            { "apple", 120 },
            { "banana", 80 },
            { "cherry", 150 }
        };
        var filtered = dict.Where(kv => kv.Value >= 100);
        Console.WriteLine("値が100以上の要素:");
        foreach (var kv in filtered)
        {
            Console.WriteLine($"{kv.Key}: {kv.Value}");
        }
    }
}
値が100以上の要素:
apple: 120
cherry: 150

キーで絞り込みたい場合はkv.Keyを使います。

例えば、キーが”a”で始まる要素を抽出する場合はkv.Key.StartsWith("a")のように指定します。

また、Dictionaryは順序を保証しないため、順序が重要な場合はSortedDictionaryOrderedDictionaryを検討してください。

LookupとIGrouping

Lookup<TKey, TElement>は、キーごとに複数の要素をグループ化したコレクションです。

GroupByメソッドの結果として得られるIEnumerable<IGrouping<TKey, TElement>>と似ていますが、Lookupは即時実行されており、キーから直接要素を取得しやすい特徴があります。

IGrouping<TKey, TElement>は、グループ化された各グループを表すインターフェースで、キーとそのグループの要素を持ちます。

以下は、Lookupを使ってグループ化し、特定のキーの要素を抽出する例です。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var people = new[]
        {
            new { Name = "Alice", Department = "Sales" },
            new { Name = "Bob", Department = "HR" },
            new { Name = "Charlie", Department = "Sales" },
            new { Name = "Diana", Department = "HR" }
        };
        var lookup = people.ToLookup(p => p.Department);
        Console.WriteLine("Sales部門のメンバー:");
        foreach (var person in lookup["Sales"])
        {
            Console.WriteLine(person.Name);
        }
    }
}
Sales部門のメンバー:
Alice
Charlie

Lookupはキーが存在しない場合、空のシーケンスを返すため、キーの存在チェックを省略できる利点があります。

一方、GroupByは遅延実行であり、必要に応じてグループを列挙します。

グループ化後にさらにフィルタリングや集計を行う場合はGroupByの方が柔軟です。

ObservableCollectionへの反映

ObservableCollection<T>は、要素の追加・削除などの変更をUIに通知できるコレクションで、WPFやUWPなどのデータバインディングでよく使われます。

LINQのフィルタリング結果はIEnumerable<T>で返されるため、そのままObservableCollection<T>に代入することはできません。

フィルタリング結果をObservableCollection<T>に反映させるには、新たにObservableCollection<T>を作成するか、既存のコレクションをクリアしてから追加します。

以下は、List<T>から条件に合う要素だけを抽出し、ObservableCollection<T>に反映する例です。

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
class Program
{
    static void Main()
    {
        var allItems = new List<string> { "Apple", "Banana", "Cherry", "Date" };
        var filtered = allItems.Where(item => item.StartsWith("B"));
        var observable = new ObservableCollection<string>(filtered);
        Console.WriteLine("ObservableCollectionの内容:");
        foreach (var item in observable)
        {
            Console.WriteLine(item);
        }
    }
}
ObservableCollectionの内容:
Banana

既に存在するObservableCollection<T>に対して更新したい場合は、以下のようにクリアしてから追加します。

observable.Clear();
foreach (var item in filtered)
{
    observable.Add(item);
}

この方法で、UIに変更通知を送りつつフィルタリング結果を反映できます。

ObservableCollection<T>は変更通知のためのイベントを持つため、直接LINQの結果を代入するだけでは通知が発生しません。

適切に反映させることが重要です。

異常値・エラー対策

NullReferenceException回避

LINQでフィルタリングを行う際に最もよく遭遇するエラーの一つがNullReferenceExceptionです。

これは、対象のオブジェクトやプロパティがnullである場合に、そのメンバーにアクセスしようとすると発生します。

特に、オブジェクトのプロパティを条件に指定する場合は注意が必要です。

例えば、以下のようなコードはPersonAddressnullの場合に例外が発生します。

using System;
using System.Collections.Generic;
using System.Linq;
class Person
{
    public string Name { get; set; }
    public Address Address { get; set; }
}
class Address
{
    public string City { get; set; }
}
class Program
{
    static void Main()
    {
        var people = new List<Person>
        {
            new Person { Name = "Alice", Address = new Address { City = "Tokyo" } },
            new Person { Name = "Bob", Address = null },
            new Person { Name = "Charlie", Address = new Address { City = "Osaka" } }
        };
        // Cityが"Tokyo"の人を抽出(nullチェックなし)
        var filtered = people.Where(p => p.Address.City == "Tokyo");
        foreach (var person in filtered)
        {
            Console.WriteLine(person.Name);
        }
    }
}

このコードはBobAddressnullなので、p.Address.Cityにアクセスした時点でNullReferenceExceptionが発生します。

回避策としては、条件式にnullチェックを入れることです。

var filtered = people.Where(p => p.Address != null && p.Address.City == "Tokyo");

また、C# 6.0以降で使えるnull条件演算子?.を使うと、より簡潔に書けます。

var filtered = people.Where(p => p.Address?.City == "Tokyo");

この場合、p.Addressnullならp.Address?.Citynullとなり、比較はfalseになるため例外は発生しません。

型変換エラー防止

LINQで異なる型のデータを扱う際、無理な型変換を行うとInvalidCastExceptionなどの型変換エラーが発生します。

特に、object型のコレクションや非ジェネリックなコレクションを扱う場合に注意が必要です。

例えば、object型のリストから整数だけを抽出しようとして、型チェックをしないと例外が起きることがあります。

using System;
using System.Collections;
using System.Linq;
class Program
{
    static void Main()
    {
        ArrayList list = new ArrayList { 1, "two", 3, "four" };
        // 直接intにキャストしようとすると例外が発生
        var numbers = list.Cast<int>().Where(n => n > 1);
        foreach (var num in numbers)
        {
            Console.WriteLine(num);
        }
    }
}

このコードは"two""four"intにキャストしようとしてInvalidCastExceptionが発生します。

対策としては、OfType<T>()メソッドを使い、指定した型の要素だけを抽出する方法があります。

var numbers = list.OfType<int>().Where(n => n > 1);

OfType<T>()は指定した型にキャスト可能な要素だけを返すため、型変換エラーを防げます。

例外を含む要素のスキップ

LINQのフィルタリング中に、条件判定の処理で例外が発生する可能性がある場合、例外をキャッチして該当要素をスキップする方法があります。

これにより、処理全体が中断されるのを防ぎ、正常な要素だけを抽出できます。

例えば、文字列を整数に変換して条件判定を行う場合、変換に失敗すると例外が発生します。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var strings = new List<string> { "10", "abc", "20", "xyz" };
        // 変換失敗で例外が発生する可能性あり
        var numbers = strings.Where(s => int.Parse(s) > 15);
        foreach (var num in numbers)
        {
            Console.WriteLine(num);
        }
    }
}

このコードは"abc""xyz"の変換でFormatExceptionが発生し、処理が中断されます。

例外を含む要素をスキップするには、try-catchを使ったヘルパーメソッドを作成し、LINQの中で呼び出す方法が有効です。

static bool TryParseAndCheck(string s, out int value)
{
    try
    {
        value = int.Parse(s);
        return value > 15;
    }
    catch
    {
        value = 0;
        return false;
    }
}
class Program
{
    static void Main()
    {
        var strings = new List<string> { "10", "abc", "20", "xyz" };
        var filtered = strings.Where(s => TryParseAndCheck(s, out _));
        foreach (var s in filtered)
        {
            Console.WriteLine(s);
        }
    }
}
20

このように、例外が発生した場合はfalseを返して該当要素を除外し、正常に処理できる要素だけを抽出しています。

また、int.TryParseを使う方法もあります。

var filtered = strings.Where(s =>
{
    bool success = int.TryParse(s, out int val);
    return success && val > 15;
});

この方法は例外を発生させずに安全に変換判定ができるため、より推奨されます。

LINQフィルタリング応用サンプル集

日付範囲での抽出

日付を扱うデータの中から、特定の期間に該当する要素だけを抽出することはよくある要件です。

LINQではDateTime型のプロパティに対して比較演算子を使い、簡単に日付範囲でのフィルタリングが可能です。

以下は、注文日時が2023年1月1日から2023年1月31日までの注文を抽出する例です。

using System;
using System.Collections.Generic;
using System.Linq;
class Order
{
    public int OrderId { get; set; }
    public DateTime OrderDate { get; set; }
}
class Program
{
    static void Main()
    {
        var orders = new List<Order>
        {
            new Order { OrderId = 1, OrderDate = new DateTime(2023, 1, 5) },
            new Order { OrderId = 2, OrderDate = new DateTime(2023, 2, 10) },
            new Order { OrderId = 3, OrderDate = new DateTime(2023, 1, 20) },
            new Order { OrderId = 4, OrderDate = new DateTime(2022, 12, 31) }
        };
        DateTime startDate = new DateTime(2023, 1, 1);
        DateTime endDate = new DateTime(2023, 1, 31);
        var filtered = orders.Where(o => o.OrderDate >= startDate && o.OrderDate <= endDate);
        Console.WriteLine("2023年1月の注文:");
        foreach (var order in filtered)
        {
            Console.WriteLine($"注文ID: {order.OrderId}, 日付: {order.OrderDate.ToShortDateString()}");
        }
    }
}
2023年1月の注文:
注文ID: 1, 日付: 2023/1/5
注文ID: 3, 日付: 2023/1/20

このように、DateTimeの比較演算子を使うことで、簡単に日付範囲の抽出ができます。

正規表現での文字列マッチ

LINQのWhereメソッド内で正規表現を使って文字列のパターンマッチを行うことも可能です。

System.Text.RegularExpressions.Regexクラスを利用して、複雑な文字列条件を指定できます。

以下は、メールアドレスのリストから、特定のドメイン(例:example.com)のメールだけを抽出する例です。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        var emails = new List<string>
        {
            "alice@example.com",
            "bob@test.com",
            "charlie@example.com",
            "diana@sample.org"
        };
        var pattern = @"^[\w\.-]+@example\.com$";
        var regex = new Regex(pattern, RegexOptions.IgnoreCase);
        var filtered = emails.Where(email => regex.IsMatch(email));
        Console.WriteLine("example.comドメインのメール:");
        foreach (var email in filtered)
        {
            Console.WriteLine(email);
        }
    }
}
example.comドメインのメール:
alice@example.com
charlie@example.com

正規表現を使うことで、単純な文字列比較では難しいパターンマッチングが可能になります。

重複除外を伴う条件

LINQのDistinctメソッドを使うと、重複した要素を除外できます。

条件付きで重複除外を行いたい場合は、Whereで絞り込んだ後にDistinctを適用するのが一般的です。

以下は、名前のリストから、20歳以上の人の名前を重複なく抽出する例です。

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 = "Alice", Age = 25 },
            new Person { Name = "Bob", Age = 18 },
            new Person { Name = "Alice", Age = 30 },
            new Person { Name = "Charlie", Age = 22 }
        };
        var distinctNames = people
            .Where(p => p.Age >= 20)
            .Select(p => p.Name)
            .Distinct();
        Console.WriteLine("20歳以上の重複しない名前:");
        foreach (var name in distinctNames)
        {
            Console.WriteLine(name);
        }
    }
}
20歳以上の重複しない名前:
Alice
Charlie

Distinctはデフォルトで要素の等価性を比較します。

カスタムクラスで独自の比較を行いたい場合は、IEqualityComparer<T>を実装して渡すことも可能です。

ページング処理とSkip/Take

大量のデータを扱う際、ページング処理で一部のデータだけを取得することが多いです。

LINQのSkipTakeメソッドを組み合わせることで、簡単にページングが実現できます。

以下は、10件ずつのページで2ページ目のデータを取得する例です。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = Enumerable.Range(1, 50);
        int pageSize = 10;
        int pageNumber = 2; // 2ページ目
        var pageData = numbers
            .Skip(pageSize * (pageNumber - 1))
            .Take(pageSize);
        Console.WriteLine($"{pageNumber}ページ目のデータ:");
        foreach (var num in pageData)
        {
            Console.WriteLine(num);
        }
    }
}
2ページ目のデータ:
11
12
13
14
15
16
17
18
19
20

Skipで前のページ分の要素を飛ばし、Takeでページサイズ分だけ取得しています。

これにより効率的なページングが可能です。

動的条件に応じたWhere生成

条件が動的に変わる場合、LINQのWhere句を柔軟に組み立てる必要があります。

複数の条件を順次追加していく方法が一般的です。

以下は、名前や年齢の条件が任意で指定される場合の例です。

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 = "Alice", Age = 25 },
            new Person { Name = "Bob", Age = 30 },
            new Person { Name = "Charlie", Age = 22 },
            new Person { Name = "Diana", Age = 28 }
        };
        string nameFilter = "a"; // 名前に'a'を含む
        int? minAge = 23;        // 23歳以上
        var query = people.AsQueryable();
        if (!string.IsNullOrEmpty(nameFilter))
        {
            query = query.Where(p => p.Name.Contains(nameFilter, StringComparison.OrdinalIgnoreCase));
        }
        if (minAge.HasValue)
        {
            query = query.Where(p => p.Age >= minAge.Value);
        }
        Console.WriteLine("動的条件でのフィルタリング結果:");
        foreach (var person in query)
        {
            Console.WriteLine($"{person.Name} ({person.Age}歳)");
        }
    }
}
動的条件でのフィルタリング結果:
Alice (25歳)
Diana (28歳)

この例では、条件がある場合のみWhereを追加してクエリを組み立てています。

AsQueryable()を使うと、LINQ to EntitiesなどのORMでも同様の書き方が可能です。

Expression Treeで複雑条件を構築

より複雑な動的条件を扱う場合、Expression<Func<T, bool>>を使って条件式をプログラム的に組み立てることができます。

これにより、複数の条件を柔軟に結合したり、条件の追加・削除が容易になります。

以下は、Expressionを使って複数条件をANDで結合する例です。

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 = "Alice", Age = 25 },
            new Person { Name = "Bob", Age = 30 },
            new Person { Name = "Charlie", Age = 22 },
            new Person { Name = "Diana", Age = 28 }
        };
        // 条件式を動的に構築
        Expression<Func<Person, bool>> condition = p => true;
        // 名前に'a'を含む条件を追加
        condition = condition.AndAlso(p => p.Name.Contains("a", StringComparison.OrdinalIgnoreCase));
        // 年齢が25歳以上の条件を追加
        condition = condition.AndAlso(p => p.Age >= 25);
        var filtered = people.AsQueryable().Where(condition);
        Console.WriteLine("Expression Treeで構築した条件の結果:");
        foreach (var person in filtered)
        {
            Console.WriteLine($"{person.Name} ({person.Age}歳)");
        }
    }
}
// Expressionの拡張メソッド
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 combined = Expression.AndAlso(
            Expression.Invoke(expr1, parameter),
            Expression.Invoke(expr2, parameter));
        return Expression.Lambda<Func<T, bool>>(combined, parameter);
    }
}
Expression Treeで構築した条件の結果:
Alice (25歳)
Diana (28歳)

この方法は、条件が多数かつ動的に変わる場合に特に有効です。

ORMのクエリ生成や複雑な検索機能の実装で活用されます。

カスタム拡張メソッドで再利用性向上

共通フィルタのメソッド化

LINQのWhereメソッドに渡す条件式は、プロジェクト内で同じような条件が繰り返し使われることがあります。

こうした共通のフィルタ条件を毎回書くのは冗長でミスの元にもなるため、拡張メソッドとして切り出すことで再利用性を高められます。

例えば、Personクラスのリストから「20歳以上かつ名前に特定の文字列を含む人」を抽出する条件を共通化してみます。

using System;
using System.Collections.Generic;
using System.Linq;
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
static class PersonExtensions
{
    // 20歳以上かつ名前に指定文字列を含む人を抽出する拡張メソッド
    public static IEnumerable<Person> FilterAdultsWithName(this IEnumerable<Person> source, string namePart)
    {
        return source.Where(p => p.Age >= 20 && p.Name.Contains(namePart, StringComparison.OrdinalIgnoreCase));
    }
}
class Program
{
    static void Main()
    {
        var people = new List<Person>
        {
            new Person { Name = "Alice", Age = 25 },
            new Person { Name = "Bob", Age = 18 },
            new Person { Name = "Charlie", Age = 30 },
            new Person { Name = "Diana", Age = 22 }
        };
        var filtered = people.FilterAdultsWithName("a");
        Console.WriteLine("20歳以上で名前に'a'を含む人:");
        foreach (var person in filtered)
        {
            Console.WriteLine($"{person.Name} ({person.Age}歳)");
        }
    }
}
20歳以上で名前に'a'を含む人:
Alice (25歳)
Charlie (30歳)
Diana (22歳)

このように拡張メソッドにすることで、呼び出し側はシンプルにFilterAdultsWithNameを呼ぶだけで済み、条件の変更やメンテナンスも一箇所で行えます。

ジェネリック制約を活かした実装

拡張メソッドをジェネリックにすることで、さまざまな型に対応した汎用的なフィルタリング処理を作成できます。

さらに、ジェネリック制約を使うと、特定のインターフェースや基底クラスを持つ型に限定して安全に処理を行えます。

例えば、INameableインターフェースを持つオブジェクトに対して、名前に特定文字列を含むかどうかでフィルタリングする拡張メソッドを作成します。

using System;
using System.Collections.Generic;
using System.Linq;
interface INameable
{
    string Name { get; }
}
class Product : INameable
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}
static class FilterExtensions
{
    // INameableを実装した型に限定した拡張メソッド
    public static IEnumerable<T> WhereNameContains<T>(this IEnumerable<T> source, string substring)
        where T : INameable
    {
        return source.Where(item => item.Name.Contains(substring, StringComparison.OrdinalIgnoreCase));
    }
}
class Program
{
    static void Main()
    {
        var products = new List<Product>
        {
            new Product { Name = "Apple", Price = 100 },
            new Product { Name = "Banana", Price = 50 },
            new Product { Name = "Grape", Price = 120 }
        };
        var filtered = products.WhereNameContains("ap");
        Console.WriteLine("名前に'ap'を含む商品:");
        foreach (var product in filtered)
        {
            Console.WriteLine($"{product.Name} ({product.Price}円)");
        }
    }
}
名前に'ap'を含む商品:
Apple (100円)
Grape (120円)

この例では、WhereNameContainsメソッドはINameableを実装した任意の型に使えます。

ジェネリック制約により、Nameプロパティが存在することが保証されているため、型安全に処理できます。

ジェネリック拡張メソッドは、共通のインターフェースや基底クラスを持つ複数の型に対して同じロジックを適用したい場合に非常に有効です。

コードの重複を減らし、保守性を向上させることができます。

デバッグとテストのポイント

クエリ結果の逐次確認

LINQクエリは遅延実行されるため、クエリを定義しただけでは実際の処理は行われません。

そのため、複雑なクエリを作成した際には、途中の結果を逐次確認しながらデバッグすることが重要です。

これにより、意図した条件で正しくフィルタリングや変換が行われているかを把握しやすくなります。

途中の結果を確認する方法としては、クエリの途中でToList()ToArray()を呼び出して即時実行し、結果を変数に格納してから内容を出力する方法があります。

以下は、複数の条件を組み合わせたクエリの途中結果を確認する例です。

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 = "Alice", Age = 25 },
            new Person { Name = "Bob", Age = 18 },
            new Person { Name = "Charlie", Age = 30 },
            new Person { Name = "Diana", Age = 22 }
        };
        // 年齢が20歳以上の人を抽出
        var adults = people.Where(p => p.Age >= 20).ToList();
        Console.WriteLine("20歳以上の人:");
        foreach (var person in adults)
        {
            Console.WriteLine($"{person.Name} ({person.Age}歳)");
        }
        // 名前に'a'を含む人をさらに抽出
        var filtered = adults.Where(p => p.Name.Contains("a", StringComparison.OrdinalIgnoreCase)).ToList();
        Console.WriteLine("\n名前に'a'を含む20歳以上の人:");
        foreach (var person in filtered)
        {
            Console.WriteLine($"{person.Name} ({person.Age}歳)");
        }
    }
}
20歳以上の人:
Alice (25歳)
Charlie (30歳)
Diana (22歳)

名前に'a'を含む20歳以上の人:
Alice (25歳)
Charlie (30歳)
Diana (22歳)

このように、途中でToList()を呼び出して結果を変数に格納することで、各段階のデータを確認しやすくなります。

Visual Studioのデバッガーを使う場合は、変数の中身をウォッチウィンドウで確認したり、ブレークポイントを設定してステップ実行しながら検証することも効果的です。

また、LINQPadなどのツールを使うと、クエリの結果を即座に確認できるため、開発効率が向上します。

Unit Testでの条件網羅

LINQクエリの正確性を保証するためには、Unit Testでさまざまな条件を網羅的にテストすることが重要です。

特にフィルタリング条件が複雑な場合や動的に変わる場合は、期待される結果が得られるかどうかを自動テストで検証することで、バグの早期発見と品質向上につながります。

以下は、xUnitを使った簡単なUnit Testの例です。

Personクラスのリストから年齢が20歳以上の人を抽出するメソッドのテストを行います。

using System.Collections.Generic;
using System.Linq;
using Xunit;
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
public class PersonFilter
{
    public static IEnumerable<Person> GetAdults(IEnumerable<Person> people)
    {
        return people.Where(p => p.Age >= 20);
    }
}
public class PersonFilterTests
{
    [Fact]
    public void GetAdults_ReturnsOnlyAdults()
    {
        var people = new List<Person>
        {
            new Person { Name = "Alice", Age = 25 },
            new Person { Name = "Bob", Age = 18 },
            new Person { Name = "Charlie", Age = 30 }
        };
        var result = PersonFilter.GetAdults(people).ToList();
        Assert.Equal(2, result.Count);
        Assert.Contains(result, p => p.Name == "Alice");
        Assert.Contains(result, p => p.Name == "Charlie");
        Assert.DoesNotContain(result, p => p.Name == "Bob");
    }
    [Fact]
    public void GetAdults_EmptyList_ReturnsEmpty()
    {
        var people = new List<Person>();
        var result = PersonFilter.GetAdults(people);
        Assert.Empty(result);
    }
}

このテストコードでは、

  • 年齢20歳以上の人だけが返されること
  • 空のリストを渡した場合は空の結果になること

を検証しています。

テストケースを増やして、境界値(例えば20歳ちょうどの人)、異常値(nullや空文字など)、複数条件の組み合わせなども網羅するとより堅牢なテストになります。

また、動的条件を扱う場合は、条件ごとに異なるテストメソッドを用意し、期待される結果を明示的に示すことが望ましいです。

Unit Testを活用することで、LINQクエリの変更やリファクタリング時に意図しない動作を防ぎ、安心してコードを保守できます。

フィルタリング後のデータ活用

JSONやCSVへのエクスポート

LINQでフィルタリングしたデータは、そのままプログラム内で利用するだけでなく、外部ファイルとして保存したり他のシステムと連携したりするためにJSONやCSV形式でエクスポートすることが多いです。

ここでは、フィルタリング後のデータをJSONとCSVに変換して保存する方法を紹介します。

JSONへのエクスポート

.NETではSystem.Text.Json名前空間のJsonSerializerクラスを使うと簡単にオブジェクトをJSON文字列にシリアライズできます。

以下は、フィルタリングしたオブジェクトのリストをJSONファイルに保存する例です。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
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 = "Alice", Age = 25 },
            new Person { Name = "Bob", Age = 18 },
            new Person { Name = "Charlie", Age = 30 }
        };
        // 20歳以上の人をフィルタリング
        var filtered = people.Where(p => p.Age >= 20).ToList();
        // JSONにシリアライズ
        string jsonString = JsonSerializer.Serialize(filtered, new JsonSerializerOptions { WriteIndented = true });
        // ファイルに書き込み
        File.WriteAllText("filtered_people.json", jsonString);
        Console.WriteLine("JSONファイルにエクスポートしました。");
    }
}
JSONファイルにエクスポートしました。

生成されるfiltered_people.jsonの内容は以下のようになります。

[
  {
    "Name": "Alice",
    "Age": 25
  },
  {
    "Name": "Charlie",
    "Age": 30
  }
]

JsonSerializerOptionsWriteIndentedtrueにすると、見やすいインデント付きのJSONが生成されます。

CSVへのエクスポート

CSV形式は表形式データの交換で広く使われています。

CSVへの変換は.NET標準で直接サポートされていないため、手動で文字列を組み立てるか、外部ライブラリ(例えばCsvHelper)を使う方法があります。

ここでは簡単な手動実装例を示します。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
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 = "Alice", Age = 25 },
            new Person { Name = "Bob", Age = 18 },
            new Person { Name = "Charlie", Age = 30 }
        };
        var filtered = people.Where(p => p.Age >= 20).ToList();
        var sb = new StringBuilder();
        // ヘッダー行
        sb.AppendLine("Name,Age");
        // データ行
        foreach (var person in filtered)
        {
            sb.AppendLine($"{EscapeCsv(person.Name)},{person.Age}");
        }
        File.WriteAllText("filtered_people.csv", sb.ToString());
        Console.WriteLine("CSVファイルにエクスポートしました。");
    }
    // CSV用にカンマやダブルクォーテーションをエスケープ
    static string EscapeCsv(string s)
    {
        if (s.Contains(",") || s.Contains("\"") || s.Contains("\n"))
        {
            s = s.Replace("\"", "\"\"");
            return $"\"{s}\"";
        }
        return s;
    }
}
CSVファイルにエクスポートしました。

生成されるfiltered_people.csvの内容は以下のようになります。

Name,Age
Alice,25
Charlie,30

このように、フィルタリング後のデータをCSV形式で保存できます。

複雑なデータや大量データの場合は、CsvHelperなどのライブラリを使うと便利です。

UIコンポーネントへのバインド

フィルタリングしたデータは、アプリケーションのUIに表示することが多いです。

WPFやWinForms、Xamarin.Forms、BlazorなどのUIフレームワークでは、データバインディング機能を使ってコレクションをUIコンポーネントに簡単に紐付けられます。

WPFでの例

WPFでは、ObservableCollection<T>を使うことで、コレクションの変更をUIに自動的に反映できます。

LINQの結果はIEnumerable<T>なので、ObservableCollection<T>に変換してバインドするのが一般的です。

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
namespace WpfApp
{
    public partial class MainWindow : Window
    {
        public ObservableCollection<Person> FilteredPeople { get; set; }
        public MainWindow()
        {
            InitializeComponent();
            var people = new List<Person>
            {
                new Person { Name = "Alice", Age = 25 },
                new Person { Name = "Bob", Age = 18 },
                new Person { Name = "Charlie", Age = 30 }
            };
            var filtered = people.Where(p => p.Age >= 20);
            FilteredPeople = new ObservableCollection<Person>(filtered);
            DataContext = this;
        }
    }
    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }
}

XAML側でListBoxなどのアイテムソースにバインドします。

<ListBox ItemsSource="{Binding FilteredPeople}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Name}" Margin="5"/>
                <TextBlock Text="{Binding Age}" Margin="5"/>
            </StackPanel>
        </DataTemplate>
    </ListBox>

このように、フィルタリングしたデータをObservableCollectionに変換してUIにバインドすると、データの変更が自動的にUIに反映されます。

WinFormsでの例

WinFormsでは、BindingSourceを使ってデータをバインドします。

LINQの結果をList<T>に変換してからBindingSource.DataSourceに設定します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
public partial class Form1 : Form
{
    private BindingSource bindingSource = new BindingSource();
    public Form1()
    {
        InitializeComponent();
        var people = new List<Person>
        {
            new Person { Name = "Alice", Age = 25 },
            new Person { Name = "Bob", Age = 18 },
            new Person { Name = "Charlie", Age = 30 }
        };
        var filtered = people.Where(p => p.Age >= 20).ToList();
        bindingSource.DataSource = filtered;
        dataGridView1.DataSource = bindingSource;
    }
}
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

このように、フィルタリングしたデータをUIコンポーネントにバインドすることで、ユーザーに動的なデータ表示を提供できます。

データの更新があれば、ObservableCollectionBindingSourceを通じてUIに反映させることが可能です。

まとめ

この記事では、C#のLINQを使ったデータフィルタリングの基本から応用まで幅広く解説しました。

Whereメソッドの使い方や複数条件の組み合わせ、パフォーマンスを意識した遅延実行の理解、よく使うメソッドとの連携方法、コレクション別の注意点、エラー対策、応用サンプル、カスタム拡張メソッドによる再利用性向上、デバッグ・テストのポイント、そしてフィルタリング後のデータ活用まで網羅しています。

これらを活用することで、効率的かつ安全にデータ操作が行え、実務での開発効率と品質向上に役立ちます。

関連記事

Back to top button
目次へ