LINQ

【C#】LINQ条件分岐の書き方と活用例:Where・Select・Anyで柔軟にフィルタリング

LINQの条件分岐はWhereで絞り込み、Selectで形を変え、AnyFirstOrDefaultで存在確認や取得を瞬時に行う流れが基本です。

ラムダ式に論理演算子を重ねれば複雑な条件も一行で書け、動的に組み立てる場合はFunc<T,bool>を差し替えるだけで柔軟に対応できます。

目次から探す
  1. LINQと条件分岐の基本
  2. 基本的なWhere句での条件分岐
  3. Selectで条件に応じて形を変える
  4. Any, First, FirstOrDefaultで存在確認と取得
  5. 複数条件の組み合わせと論理演算子
  6. 動的に組み立てる条件式
  7. null値とNullable型の安全な扱い
  8. パフォーマンスを意識した書き方
  9. パターン別サンプル集
  10. よくある落とし穴とデバッグポイント
  11. まとめ

LINQと条件分岐の基本

LINQが提供するクエリ機能

C#のLINQ(Language Integrated Query)は、コレクションやデータソースに対して一貫した方法でクエリを記述できる機能です。

LINQを使うと、配列やリスト、データベース、XMLなど様々なデータに対して、SQLのような直感的なクエリをコード内で直接書けます。

LINQの主な特徴は以下の通りです。

  • 統一されたクエリ構文

配列やリスト、データベースなど異なるデータソースに対しても、同じような書き方でデータの抽出や変換が可能です。

  • 遅延実行

クエリは定義した時点では実行されず、実際に結果を使うタイミングで処理が行われます。

これにより効率的なデータ処理が可能です。

  • 豊富なメソッド群

Where(条件で絞り込み)、Select(変換)、OrderBy(並べ替え)、GroupBy(グループ化)など、多彩なメソッドが用意されています。

  • 型安全

コンパイル時に型チェックが行われるため、実行時のエラーを減らせます。

例えば、配列から偶数だけを抽出する場合、LINQを使うと以下のように書けます。

int[] numbers = { 1, 2, 3, 4, 5, 6 };
var evenNumbers = numbers.Where(n => n % 2 == 0);
Console.WriteLine(string.Join(", ", evenNumbers)); // 出力: 2, 4, 6

このように、LINQはデータの抽出や変換を簡潔に記述できるため、コードの可読性と保守性が向上します。

条件分岐を埋め込むメリット

LINQのクエリに条件分岐を組み込むことで、データのフィルタリングや変換を柔軟に行えます。

条件分岐を活用する主なメリットは以下の通りです。

  • 柔軟なフィルタリング

Whereメソッドに条件式を渡すことで、必要なデータだけを抽出できます。

条件は単純な比較から複雑な論理式まで自由に組み合わせられます。

  • 動的な条件設定

実行時の状況に応じて条件を変えられるため、ユーザー入力や設定に基づく検索機能などに適しています。

  • コードの簡潔化

条件分岐をクエリ内に直接書けるため、従来のループやif文でのフィルタリングに比べてコードが短くなり、読みやすくなります。

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

LINQは遅延実行のため、条件に合致する要素が見つかれば処理を早期に終了することも可能です。

特にAnyメソッドなどは効率的に存在チェックができます。

  • 変換処理との組み合わせ

条件に応じてSelectで変換を行うことで、必要な形にデータを整形しながら抽出できます。

例えば、ユーザーの年齢が20歳以上かつ名前に「a」が含まれる人だけを抽出する場合、以下のように書けます。

var filteredUsers = users.Where(u => u.Age >= 20 && u.Name.Contains("a"));

このように条件分岐を埋め込むことで、必要なデータだけを効率よく取得できるのがLINQの大きな強みです。

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

LINQの条件分岐では、主にデリゲートラムダ式が使われます。

これらはC#の関数型プログラミング的な要素で、条件をコードとして渡すための仕組みです。

デリゲートとは

デリゲートは「メソッドへの参照を保持する型」です。

簡単に言うと、関数やメソッドを変数のように扱える仕組みです。

LINQのWhereメソッドは、条件を判定する関数を引数に取りますが、この関数はFunc<T, bool>というデリゲート型で表現されます。

例えば、以下のようにデリゲートを使って条件を定義できます。

Func<int, bool> isEven = delegate(int n) { return n % 2 == 0; };
var evenNumbers = numbers.Where(isEven);

ラムダ式とは

ラムダ式は、匿名関数を簡潔に書くための構文です。

上記のデリゲート定義は、ラムダ式を使うともっと短く書けます。

var evenNumbers = numbers.Where(n => n % 2 == 0);

ここでn => n % 2 == 0がラムダ式です。

左側のnが引数、右側が戻り値の式を表しています。

LINQでの活用

LINQのメソッドは、条件や変換のロジックをデリゲートとして受け取ります。

ラムダ式はこのデリゲートを簡単に記述できるため、LINQのクエリは非常にシンプルになります。

例えば、WhereメソッドはFunc<T, bool>型の条件関数を受け取るため、以下のように書けます。

var adults = people.Where(p => p.Age >= 20);

このp => p.Age >= 20がラムダ式で、pPerson型の引数、p.Age >= 20が条件判定の戻り値です。

  • デリゲートはメソッドを変数のように扱う型
  • ラムダ式は匿名関数を簡潔に書く構文
  • LINQはデリゲートを引数に取るため、ラムダ式で条件や変換を記述する
  • これにより、条件分岐を柔軟かつ簡単にクエリに組み込める

このように、デリゲートとラムダ式の理解はLINQの条件分岐を使いこなす上で欠かせません。

基本的なWhere句での条件分岐

単一条件フィルタリング

数値比較

数値の条件分岐はLINQのWhere句で最もよく使われるパターンです。

例えば、配列やリストの中から特定の数値条件を満たす要素だけを抽出できます。

以下は、整数配列から5より大きい数値を抽出する例です。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 1, 3, 5, 7, 9 };
        var filtered = numbers.Where(n => n > 5);
        Console.WriteLine(string.Join(", ", filtered)); // 出力: 7, 9
    }
}
7, 9

この例では、n > 5という単純な条件でフィルタリングしています。

比較演算子は>, <, >=, <=, ==, !=が使えます。

また、複数の数値条件を組み合わせる場合もありますが、単一条件の場合はこのようにシンプルに書けます。

文字列比較

文字列に対する条件分岐もよく使われます。

文字列の比較は==!=のほか、ContainsStartsWithEndsWithなどのメソッドを使うことが多いです。

例えば、名前のリストから「John」という名前だけを抽出する例です。

using System;
using System.Linq;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        List<string> names = new List<string> { "John", "Jane", "Jack", "Johnny" };
        var filtered = names.Where(name => name == "John");
        Console.WriteLine(string.Join(", ", filtered)); // 出力: John
    }
}
John

部分一致で「John」を含む名前を抽出する場合はContainsを使います。

List<string> names = new List<string> { "John", "Jane", "Jack", "Johnny" };
var filteredContains = names.Where(name => name.Contains("John"));
Console.WriteLine(string.Join(", ", filteredContains)); // 出力: John, Johnny
John, Johnny

StartsWithEndsWithも同様に使えます。

List<string> names = new List<string> { "John", "Jane", "Jack", "Johnny" };
var startsWithJ = names.Where(name => name.StartsWith("J"));
Console.WriteLine(string.Join(", ", startsWithJ)); // 出力: John, Jane, Jack, Johnny
John, Jane, Jack, Johnny

文字列比較は大文字・小文字を区別するため、区別したくない場合はStringComparisonを指定するか、ToLower()などで統一してから比較します。

List<string> names = new List<string> { "John", "Jane", "Jack", "Johnny" };
var filteredIgnoreCase = names.Where(name => name.ToLower().Contains("john"));
Console.WriteLine(string.Join(", ", filteredIgnoreCase)); // 出力: John, Johnny
John, Johnny

論理演算子による複数条件

AND条件

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

例えば、数値のリストから3以上かつ7以下の数値を抽出する例です。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 1, 3, 5, 7, 9 };
        var filtered = numbers.Where(n => n >= 3 && n <= 7);
        Console.WriteLine(string.Join(", ", filtered)); // 出力: 3, 5, 7
    }
}
3, 5, 7

文字列でも複数条件を組み合わせられます。

例えば、名前が「J」で始まり、かつ5文字以上の名前を抽出する例です。

List<string> names = new List<string> { "John", "Jane", "Jack", "Johnny" };
var filteredNames = names.Where(name => name.StartsWith("J") && name.Length >= 5);
Console.WriteLine(string.Join(", ", filteredNames)); // 出力: Johnny
Johnny

OR条件

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

例えば、数値のリストから2未満または8以上の数値を抽出する例です。

int[] numbers = { 1, 3, 5, 7, 9 };
var filtered = numbers.Where(n => n < 2 || n >= 8);
Console.WriteLine(string.Join(", ", filtered)); // 出力: 1, 9
1, 9

文字列でも同様に使えます。

名前が「John」または「Jane」のどちらかに一致するものを抽出します。

List<string> names = new List<string> { "John", "Jane", "Jack", "Johnny" };
var filteredNames = names.Where(name => name == "John" || name == "Jane");
Console.WriteLine(string.Join(", ", filteredNames)); // 出力: John, Jane
John, Jane

否定条件

条件を満たさない要素を抽出するには、否定演算子!を使います。

例えば、数値のリストから5以外の数値を抽出する例です。

int[] numbers = { 1, 3, 5, 7, 9 };
var filtered = numbers.Where(n => n != 5);
Console.WriteLine(string.Join(", ", filtered)); // 出力: 1, 3, 7, 9
1, 3, 7, 9

文字列でも使えます。

名前が「Jack」でないものを抽出します。

List<string> names = new List<string> { "John", "Jane", "Jack", "Johnny" };
var filteredNames = names.Where(name => name != "Jack");
Console.WriteLine(string.Join(", ", filteredNames)); // 出力: John, Jane, Johnny
John, Jane, Johnny

否定条件は複雑な条件式の中で使うことも多いので、括弧を使って優先順位を明確にすると読みやすくなります。

int[] numbers = { 1, 3, 5, 7, 9 };
var filtered = numbers.Where(n => !(n > 3 && n < 8));
Console.WriteLine(string.Join(", ", filtered)); // 出力: 1, 3, 9
1, 3, 9

このように、Where句では単一条件から複数条件まで、論理演算子を使って柔軟に条件分岐を記述できます。

条件式はラムダ式で簡潔に書けるため、読みやすく保守しやすいコードになります。

Selectで条件に応じて形を変える

条件に基づく匿名型の生成

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

条件に応じて匿名型を生成することで、必要な情報だけを抽出しつつ、柔軟にデータ構造を変えられます。

例えば、ユーザーのリストから年齢が20歳以上かどうかで表示内容を変えた匿名型を作成する例です。

using System;
using System.Collections.Generic;
using System.Linq;
class User
{
    public string Name { get; set; }
    public int Age { get; set; }
}
class Program
{
    static void Main()
    {
        var users = new List<User>
        {
            new User { Name = "Alice", Age = 25 },
            new User { Name = "Bob", Age = 17 },
            new User { Name = "Charlie", Age = 30 }
        };
        var result = users.Select(u => new
        {
            u.Name,
            Status = u.Age >= 20 ? "Adult" : "Minor"
        });
        foreach (var item in result)
        {
            Console.WriteLine($"{item.Name}: {item.Status}");
        }
    }
}
Alice: Adult
Bob: Minor
Charlie: Adult

この例では、Select内で匿名型を生成し、Statusプロパティに年齢条件に基づく文字列を設定しています。

匿名型は型名を定義せずに複数のプロパティをまとめられるため、簡潔にデータを整形できます。

条件演算子(三項演算子)の活用

条件演算子?:は、Selectの中で条件に応じて値を切り替えるのに便利です。

上の例でも使いましたが、より複雑な条件でも活用できます。

例えば、商品の価格に応じて割引率を変える例です。

using System;
using System.Collections.Generic;
using System.Linq;
class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}
class Program
{
    static void Main()
    {
        var products = new List<Product>
        {
            new Product { Name = "Pen", Price = 100 },
            new Product { Name = "Notebook", Price = 500 },
            new Product { Name = "Bag", Price = 2000 }
        };
        var discounted = products.Select(p => new
        {
            p.Name,
            DiscountRate = p.Price > 1000 ? 0.2m : 0.1m,
            DiscountedPrice = p.Price * (p.Price > 1000 ? 0.8m : 0.9m)
        });
        foreach (var item in discounted)
        {
            Console.WriteLine($"{item.Name}: 割引率 {item.DiscountRate:P0}, 割引後価格 {item.DiscountedPrice}円");
        }
    }
}
Pen: 割引率 10%, 割引後価格 90.0円
Notebook: 割引率 10%, 割引後価格 450.0円
Bag: 割引率 20%, 割引後価格 1600.0円

この例では、価格が1000円を超えるかどうかで割引率と割引後価格を切り替えています。

三項演算子を使うことで、Select内の条件分岐が簡潔に書けます。

条件ごとに異なる型へマップするテクニック

Selectで条件に応じて異なる型にマップする場合、通常は共通の基底型やインターフェースを使うか、匿名型ではなく明示的なクラスを用意します。

匿名型は型が異なると扱いにくいためです。

例えば、動物のリストから犬と猫で異なるクラスにマップする例を示します。

using System;
using System.Collections.Generic;
using System.Linq;
abstract class Animal
{
    public string Name { get; set; }
    public abstract void Speak();
}
class Dog : Animal
{
    public override void Speak() => Console.WriteLine($"{Name} says: ワンワン");
}
class Cat : Animal
{
    public override void Speak() => Console.WriteLine($"{Name} says: ニャー");
}
class Program
{
    static void Main()
    {
        var animals = new List<(string Type, string Name)>
        {
            ("Dog", "Pochi"),
            ("Cat", "Tama"),
            ("Dog", "Shiro")
        };
        var mapped = animals.Select(a =>
            a.Type == "Dog" ? (Animal)new Dog { Name = a.Name } :
            a.Type == "Cat" ? (Animal)new Cat { Name = a.Name } :
            null
        ).Where(a => a != null);
        foreach (var animal in mapped)
        {
            animal.Speak();
        }
    }
}
Pochi says: ワンワン
Tama says: ニャー
Shiro says: ワンワン

この例では、タプルのTypeに応じてDogCatのインスタンスを生成し、共通の基底クラスAnimalとして扱っています。

Select内で条件演算子を使い、異なる型のオブジェクトを返しています。

匿名型ではなく基底クラスやインターフェースを使うことで、異なる型のオブジェクトを一つのコレクションにまとめて扱えます。

また、条件が複雑な場合はswitch式を使うこともできます。

var mapped = animals.Select(a => a.Type switch
{
    "Dog" => (Animal)new Dog { Name = a.Name },
    "Cat" => new Cat { Name = a.Name },
    _ => null
}).Where(a => a != null);

このように、Selectで条件に応じて異なる型にマップする場合は、共通の基底型を用意し、条件演算子やswitch式でインスタンスを切り替えるのが一般的です。

Any, First, FirstOrDefaultで存在確認と取得

要素が存在するか調べるAny

LINQのAnyメソッドは、コレクション内に条件を満たす要素が1つでも存在するかどうかを判定します。

条件を指定しない場合は、コレクションが空でないかどうかをチェックします。

Anyは条件に合致する要素が見つかると即座に処理を終了するため、大きなコレクションでも効率的に存在確認ができます。

空集合チェック

コレクションが空かどうかを調べるには、Anyを条件なしで使います。

空でなければtrue、空ならfalseを返します。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var emptyList = new List<int>();
        var numbers = new List<int> { 1, 2, 3 };
        Console.WriteLine(emptyList.Any()); // 出力: False
        Console.WriteLine(numbers.Any());   // 出力: True
    }
}
False
True

このように、Anyは空集合かどうかの判定に便利です。

Count()を使うよりもパフォーマンスが良いので、空チェックにはAnyを使うことが推奨されます。

部分一致チェック

条件を指定して、条件を満たす要素が存在するかどうかを調べることもできます。

例えば、数値のリストに偶数が存在するかを調べる例です。

var numbers = new List<int> { 1, 3, 5, 6, 7 };
bool hasEven = numbers.Any(n => n % 2 == 0);
Console.WriteLine(hasEven); // 出力: True
True

文字列のリストで特定の文字列を含む要素があるかどうかも同様に調べられます。

var names = new List<string> { "Alice", "Bob", "Charlie" };
bool hasNameWithA = names.Any(name => name.Contains("a") || name.Contains("A"));
Console.WriteLine(hasNameWithA); // 出力: True
True

先頭要素を取得するFirst

Firstメソッドは、条件を満たす最初の要素を取得します。

条件を指定しない場合は、コレクションの最初の要素を返します。

ただし、条件を満たす要素が存在しない場合や、空のコレクションに対して呼び出すとInvalidOperationExceptionがスローされるため、事前に存在確認を行うか例外処理が必要です。

var numbers = new List<int> { 1, 3, 5, 6, 7 };
int firstEven = numbers.First(n => n % 2 == 0);
Console.WriteLine(firstEven); // 出力: 6
6

条件を満たす要素がない場合の例外発生を防ぐため、Anyで存在確認を行うか、FirstOrDefaultを使う方法があります。

デフォルト値を返すFirstOrDefault

FirstOrDefaultは、条件を満たす最初の要素を返しますが、条件に合致する要素がない場合は型のデフォルト値を返します。

参照型ならnull、値型なら0falseなどのデフォルト値です。

例外が発生しないため、安全に使えます。

var numbers = new List<int> { 1, 3, 5 };
int firstEven = numbers.FirstOrDefault(n => n % 2 == 0);
Console.WriteLine(firstEven); // 出力: 0(デフォルト値)
0
var names = new List<string> { "Alice", "Bob" };
string firstNameWithZ = names.FirstOrDefault(name => name.StartsWith("Z"));
Console.WriteLine(firstNameWithZ == null ? "該当なし" : firstNameWithZ); // 出力: 該当なし
該当なし

値型と参照型のデフォルトの違い

FirstOrDefaultが返すデフォルト値は、型によって異なります。

型の種類デフォルト値の例
値型0(int)、false(bool)など
参照型null

例えば、int型のリストで条件に合う要素がない場合は0が返りますが、string型のリストではnullが返ります。

この違いを理解しておかないと、値型のデフォルト値が有効なデータと区別できず、意図しない動作になることがあります。

必要に応じて、Nullable<T>型を使うか、Anyで存在確認を行うことが安全です。

var numbers = new List<int> { 1, 3, 5 };
int? firstEvenNullable = numbers.Cast<int?>().FirstOrDefault(n => n.HasValue && n % 2 == 0);
Console.WriteLine(firstEvenNullable.HasValue ? firstEvenNullable.Value.ToString() : "該当なし"); // 出力: 該当なし
該当なし

このように、AnyFirstFirstOrDefaultはそれぞれ用途に応じて使い分けることで、存在確認や要素取得を安全かつ効率的に行えます。

複数条件の組み合わせと論理演算子

入れ子になった条件

複数の条件を組み合わせる際、条件式の中にさらに条件式を入れ子にすることがあります。

入れ子の条件は、複雑な論理判定を行う場合に役立ちます。

C#では括弧を使って条件の優先順位を明確にし、意図した通りに評価されるようにします。

例えば、年齢が20歳以上かつ(名前が「Alice」または「Bob」)である人を抽出する場合は以下のように書きます。

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 = 19 },
            new Person { Name = "Charlie", Age = 30 },
            new Person { Name = "Bob", Age = 22 }
        };
        var filtered = people.Where(p => p.Age >= 20 && (p.Name == "Alice" || p.Name == "Bob"));
        foreach (var person in filtered)
        {
            Console.WriteLine($"{person.Name}, {person.Age}");
        }
    }
}
Alice, 25
Bob, 22

この例では、(p.Name == "Alice" || p.Name == "Bob")の部分を括弧で囲むことで、&&よりも先に評価されるようにしています。

入れ子の条件は複雑な論理を正しく表現するために重要です。

メソッドチェーンでの複数Where

LINQではWhereメソッドを複数回チェーンして使うこともできます。

複数のWhereを連続して書くと、それぞれの条件で順にフィルタリングが行われます。

例えば、数値のリストから3以上の数値を抽出し、その中から偶数だけをさらに抽出する例です。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3, 4, 5, 6, 7 };
        var filtered = numbers.Where(n => n >= 3).Where(n => n % 2 == 0);
        Console.WriteLine(string.Join(", ", filtered)); // 出力: 4, 6
    }
}
4, 6

このように複数のWhereを使うと、条件ごとに分けて記述できるため、可読性が向上する場合があります。

ただし、内部的にはすべての条件が結合されて処理されるため、パフォーマンスに大きな差はありません。

クエリ構文での複数where句

LINQのクエリ構文でも複数のwhere句を使えます。

メソッドチェーンと同様に、複数の条件を段階的に指定できます。

以下は、クエリ構文で3以上の数値かつ偶数の数値を抽出する例です。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3, 4, 5, 6, 7 };
        var filtered = from n in numbers
                       where n >= 3
                       where n % 2 == 0
                       select n;
        Console.WriteLine(string.Join(", ", filtered)); // 出力: 4, 6
    }
}
4, 6

クエリ構文のwhere句は複数書けるため、条件を分けて記述したい場合に便利です。

メソッドチェーンと同様に、条件はANDで結合されます。

括弧の優先順位と可読性

複雑な条件式を書く際は、括弧を使って論理演算子の優先順位を明確にすることが重要です。

C#の論理演算子の優先順位は以下の通りです。

演算子優先順位(高い順)
!1
&&2
||3

つまり、!(否定)が最も優先され、次に&&(AND)、最後に||(OR)が評価されます。

例えば、以下の条件式は意図しない結果になる可能性があります。

var filtered = numbers.Where(n => n > 3 && n < 7 || n == 10);

この場合、&&||より優先されるため、(n > 3 && n < 7) || n == 10と解釈されます。

もし意図が異なる場合は、括弧で明示的に優先順位を指定します。

var filtered = numbers.Where(n => n > 3 && (n < 7 || n == 10));

括弧を使うことで、条件の意味が明確になり、可読性も向上します。

複雑な条件式を書く際は、必ず括弧を使って優先順位を明示しましょう。

また、条件式が長くなる場合は、変数に分割して意味を持たせるとさらに読みやすくなります。

var isInRange = n < 7 || n == 10;
var filtered = numbers.Where(n => n > 3 && isInRange);

このように、複数条件の組み合わせでは括弧の使い方と論理演算子の優先順位を理解し、可読性を意識したコードを書くことが大切です。

動的に組み立てる条件式

Func<T,bool>による可変条件

LINQの条件式はFunc<T, bool>型のデリゲートで表現されます。

これを活用すると、実行時に条件を動的に組み立てて柔軟にフィルタリングが可能です。

例えば、ユーザーの入力や設定に応じて条件を切り替えたい場合に便利です。

以下は、Func<int, bool>を使って条件を動的に切り替える例です。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3, 4, 5, 6 };
        bool filterEven = true; // 条件を切り替えるフラグ
        Func<int, bool> condition = n => true; // デフォルトはすべて通過
        if (filterEven)
        {
            condition = n => n % 2 == 0;
        }
        var filtered = numbers.Where(condition);
        Console.WriteLine(string.Join(", ", filtered)); // 出力: 2, 4, 6
    }
}
2, 4, 6

このように、Func<T, bool>を変数に代入して条件を切り替えられます。

ただし、Func<T, bool>はメモリ上の関数であり、LINQ to Objectsでは問題ありませんが、LINQ to Entities(Entity Frameworkなど)では式ツリーが必要になるため注意が必要です。

Expression<Func<T,bool>>でのLINQ to Entities対応

Entity FrameworkなどのLINQ to Entitiesでは、クエリをSQLに変換するために式ツリーExpression<Func<T, bool>>が必要です。

Func<T, bool>は実行時のメソッド参照であり、SQLに変換できません。

動的に条件を組み立てる場合は、Expression<Func<T, bool>>を使う必要があります。

これにより、条件式を構文木として表現し、LINQ to EntitiesがSQLに変換可能になります。

以下は、Expression<Func<int, bool>>を使って条件を動的に切り替える例です。

using System;
using System.Linq;
using System.Linq.Expressions;
class Program
{
    static void Main()
    {
        var numbers = new[] { 1, 2, 3, 4, 5, 6 }.AsQueryable();
        bool filterEven = true;
        Expression<Func<int, bool>> condition = n => true;
        if (filterEven)
        {
            condition = n => n % 2 == 0;
        }
        var filtered = numbers.Where(condition);
        Console.WriteLine(string.Join(", ", filtered)); // 出力: 2, 4, 6
    }
}
2, 4, 6

Expression<Func<T, bool>>は式ツリーとして条件を表現するため、LINQ to Entitiesでの動的クエリに適しています。

条件の追加と削除を行う拡張メソッド

動的に条件を組み立てる際、条件の追加や削除を簡単に行える拡張メソッドを作ると便利です。

特に、条件が真の場合のみWhereを適用するようなメソッドはよく使われます。

WhereIf拡張メソッド

WhereIfは、指定した条件がtrueの場合のみWhereを適用し、falseの場合は元のクエリをそのまま返す拡張メソッドです。

これにより、条件付きでフィルタリングを簡潔に書けます。

using System;
using System.Linq;
using System.Linq.Expressions;
public static class IQueryableExtensions
{
    public static IQueryable<T> WhereIf<T>(this IQueryable<T> source, bool condition, Expression<Func<T, bool>> predicate)
    {
        if (condition)
        {
            return source.Where(predicate);
        }
        else
        {
            return source;
        }
    }
}

実装アイデア

  • sourceは元のIQueryable<T>コレクション
  • conditiontrueならpredicateを使ってWhereを適用
  • falseなら元のsourceをそのまま返す
  • Expression<Func<T, bool>>を使うことでLINQ to Entitiesにも対応可能

使用例

using System;
using System.Collections.Generic;
using System.Linq;
class User
{
    public string Name { get; set; }
    public int Age { get; set; }
}
class Program
{
    static void Main()
    {
        var users = new List<User>
        {
            new User { Name = "Alice", Age = 25 },
            new User { Name = "Bob", Age = 17 },
            new User { Name = "Charlie", Age = 30 }
        }.AsQueryable();
        bool filterAdults = true;
        bool filterNameContainsA = false;
        var query = users
            .WhereIf(filterAdults, u => u.Age >= 20)
            .WhereIf(filterNameContainsA, u => u.Name.Contains("a"));
        foreach (var user in query)
        {
            Console.WriteLine($"{user.Name}, {user.Age}");
        }
    }
}
Alice, 25
Charlie, 30

この例では、filterAdultstrueなので年齢20以上の条件が適用され、filterNameContainsAfalseなので名前の条件は無視されています。

WhereIfを使うことで条件付きのフィルタリングが簡潔に書けます。

null値とNullable型の安全な扱い

NullReferenceExceptionを防ぐパターン

C#でLINQを使う際、null値が含まれるコレクションやプロパティを扱うと、NullReferenceExceptionが発生するリスクがあります。

特に、参照型のプロパティにアクセスする際や、Nullable<T>型の値を扱う場合は注意が必要です。

例えば、以下のようにPersonクラスのAddressプロパティがnullの場合にNullReferenceExceptionが発生します。

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 }
        };
        // NullReferenceExceptionが発生する可能性あり
        var filtered = people.Where(p => p.Address.City == "Tokyo");
        foreach (var person in filtered)
        {
            Console.WriteLine(person.Name);
        }
    }
}

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

これを防ぐには、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の場合はnullが返り、== "Tokyo"の比較はfalseとなるため安全です。

null合体演算子と条件演算子の併用

null値を扱う際に便利なのがnull合体演算子??です。

これは左辺がnullの場合に右辺の値を返します。

条件演算子?:と組み合わせることで、nullを含む値の安全な処理が可能です。

例えば、Nullable<int>型の年齢を持つユーザーのリストで、年齢がnullの場合は「不明」と表示し、それ以外は年齢を表示する例です。

using System;
using System.Collections.Generic;
using System.Linq;
class User
{
    public string Name { get; set; }
    public int? Age { get; set; }
}
class Program
{
    static void Main()
    {
        var users = new List<User>
        {
            new User { Name = "Alice", Age = 25 },
            new User { Name = "Bob", Age = null }
        };
        var result = users.Select(u => new
        {
            u.Name,
            AgeDisplay = u.Age?.ToString() ?? "不明"
        });
        foreach (var item in result)
        {
            Console.WriteLine($"{item.Name}: {item.AgeDisplay}");
        }
    }
}
Alice: 25
Bob: 不明

この例では、u.Age?.ToString()nullの場合に"不明"を返すため、null値を安全に文字列化しています。

条件演算子を使う場合は以下のように書けます。

AgeDisplay = u.Age.HasValue ? u.Age.Value.ToString() : "不明"

どちらもnull値を扱う際に役立つテクニックです。

参照型nullと値型nullの違い

C#では、参照型と値型でnullの扱いが異なります。

種類null許容nullの意味
参照型可能string, classオブジェクトが存在しないことを示す
値型不可int, bool, DateTimeそもそもnullを持てない
Nullable値型可能 (Nullable<T>)int?, bool?, DateTime?値が存在しないことを示す

値型は通常nullを持てませんが、Nullable<T>T?を使うことでnullを許容できます。

これにより、データベースのNULL値や未設定の状態を表現できます。

例えば、int?型の変数は以下のように使います。

int? nullableInt = null;
if (nullableInt.HasValue)
{
    Console.WriteLine(nullableInt.Value);
}
else
{
    Console.WriteLine("値がありません");
}

LINQでNullable<T>を扱う場合は、HasValueValueプロパティを使ってnullチェックを行うことが多いです。

var adults = users.Where(u => u.Age.HasValue && u.Age.Value >= 20);

参照型のnullはオブジェクトの不在を示し、値型のnullは値の未設定を示すため、用途に応じて使い分けることが重要です。

これらの違いを理解し、null安全なコードを書くことで、NullReferenceExceptionを防ぎつつ柔軟なデータ処理が可能になります。

パフォーマンスを意識した書き方

単一Whereと複数Whereの性能差

LINQのWhereメソッドは複数回チェーンして使うことができますが、単一のWhereに複数条件をまとめる場合と比べて性能に違いがあるか気になることがあります。

例えば、以下の2つのコードは同じ条件を表しています。

// 複数Whereをチェーン
var filtered1 = numbers.Where(n => n > 10).Where(n => n % 2 == 0);
// 単一Whereにまとめる
var filtered2 = numbers.Where(n => n > 10 && n % 2 == 0);

内部的には、LINQの遅延実行により両者はほぼ同じ処理になります。

Whereはイテレータを返すため、複数のWhereは条件を順に適用する形で処理されますが、最終的に全ての条件が満たされる要素だけが返されます。

パフォーマンス差はほとんど無視できるレベルであり、可読性やメンテナンス性を優先して書くのが一般的です。

ただし、条件が非常に複雑であったり、パフォーマンスが極めて重要な場合は、単一のWhereにまとめることでわずかな効率化が期待できます。

早期終了が期待できるAnyの活用

Anyメソッドは条件を満たす要素が見つかった時点で処理を終了するため、存在確認に非常に効率的です。

大きなコレクションで条件に合う要素が早期に見つかる場合、全要素を走査する必要がなくなります。

例えば、以下のコードは偶数が存在するかどうかを調べます。

bool hasEven = numbers.Any(n => n % 2 == 0);

この場合、最初に偶数が見つかると即座にtrueを返し、残りの要素は評価されません。

これにより、無駄な処理を減らしパフォーマンスを向上させられます。

一方、CountWhereで条件を満たす要素をすべて列挙してから判定する方法は、全要素を評価するため非効率です。

存在確認にはAnyを使うことが推奨されます。

コレクションサイズが大きい場合の評価タイミング

遅延実行と即時実行

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

つまり、クエリを定義しただけでは処理は行われず、結果を列挙するタイミングで初めて評価されます。

var query = numbers.Where(n => n > 10); // ここではまだ処理されない
var result = query.ToList();             // ここで初めて評価される

遅延実行のメリットは、必要な時に必要なだけ処理を行うため、無駄な計算を避けられることです。

しかし、大きなコレクションに対して複数回列挙すると、そのたびにクエリが再評価されるため、パフォーマンスに悪影響を及ぼすことがあります。

一方、即時実行ToList()ToArray()などのメソッドを使って、クエリの結果を一度に評価しメモリに格納することです。

これにより、複数回の列挙でも再評価が発生しません。

ToListやToArrayの扱い

ToList()ToArray()は即時実行をトリガーし、クエリの結果をメモリ上にコピーします。

大きなコレクションの場合、メモリ使用量が増えるため注意が必要です。

var filteredList = numbers.Where(n => n > 10).ToList();

このコードは、条件を満たすすべての要素をリストに格納します。

以降はfilteredListを何度でも列挙できますが、初回のToList()呼び出し時に全要素を評価するため、処理時間がかかります。

パフォーマンスを考慮すると、以下のポイントに注意してください。

  • 遅延実行のまま使う場合
    • 可能な限り一度だけ列挙する
    • 複数回列挙する場合は即時実行で結果をキャッシュする
  • 即時実行を使う場合
    • メモリ使用量に注意する
    • 大量データの場合は必要な範囲だけを抽出する工夫をする

まとめると、LINQの遅延実行と即時実行の特性を理解し、コレクションのサイズや使用シーンに応じて適切に使い分けることがパフォーマンス向上につながります。

パターン別サンプル集

日付で範囲フィルタリング

日付を使った範囲フィルタリングは、イベントやログ、予約情報などでよく使われます。

LINQのWhere句で開始日と終了日の範囲内にあるデータを抽出する例を示します。

using System;
using System.Collections.Generic;
using System.Linq;
class Event
{
    public string Title { get; set; }
    public DateTime Date { get; set; }
}
class Program
{
    static void Main()
    {
        var events = new List<Event>
        {
            new Event { Title = "Meeting", Date = new DateTime(2024, 6, 1) },
            new Event { Title = "Conference", Date = new DateTime(2024, 6, 15) },
            new Event { Title = "Workshop", Date = new DateTime(2024, 7, 5) }
        };
        DateTime startDate = new DateTime(2024, 6, 1);
        DateTime endDate = new DateTime(2024, 6, 30);
        var filtered = events.Where(e => e.Date >= startDate && e.Date <= endDate);
        foreach (var ev in filtered)
        {
            Console.WriteLine($"{ev.Title} - {ev.Date:yyyy-MM-dd}");
        }
    }
}
Meeting - 2024-06-01
Conference - 2024-06-15

この例では、6月1日から6月30日までのイベントを抽出しています。

日付の比較は>=<=を使い、範囲を指定します。

列挙型で条件分岐

列挙型(enum)を使った条件分岐は、状態管理やカテゴリ分けに便利です。

LINQのWhereで特定の列挙値に該当する要素を抽出する例を示します。

using System;
using System.Collections.Generic;
using System.Linq;
enum OrderStatus
{
    Pending,
    Shipped,
    Delivered,
    Cancelled
}
class Order
{
    public int Id { get; set; }
    public OrderStatus Status { get; set; }
}
class Program
{
    static void Main()
    {
        var orders = new List<Order>
        {
            new Order { Id = 1, Status = OrderStatus.Pending },
            new Order { Id = 2, Status = OrderStatus.Shipped },
            new Order { Id = 3, Status = OrderStatus.Delivered },
            new Order { Id = 4, Status = OrderStatus.Cancelled }
        };
        var activeOrders = orders.Where(o => o.Status == OrderStatus.Pending || o.Status == OrderStatus.Shipped);
        foreach (var order in activeOrders)
        {
            Console.WriteLine($"Order {order.Id} - {order.Status}");
        }
    }
}
Order 1 - Pending
Order 2 - Shipped

この例では、PendingまたはShippedの注文だけを抽出しています。

列挙型はコードの可読性と安全性を高めます。

ユーザー入力による検索条件

ユーザー入力に応じて検索条件を動的に変える場合、条件を組み立ててLINQに渡すことが多いです。

以下は、名前と年齢の検索条件をユーザー入力で切り替える例です。

// ユーザークラス
class User
{
    public string Name { get; set; }
    public int Age { get; set; }
}

class Program
{
    static void Main()
    {
        // 検索対象のユーザー一覧
        var users = new List<User>
        {
            new User { Name = "Alice",   Age = 25 },
            new User { Name = "Bob",     Age = 30 },
            new User { Name = "Charlie", Age = 35 }
        };

        // 入力条件 (部分一致・大文字小文字不問)
        string inputName = "a";
        // 入力条件 (年齢以上)
        int? inputAge = 30;

        // LINQ クエリのベース
        var query = users.AsQueryable();

        // 名前の部分一致検索 (大文字小文字不問)
        if (!string.IsNullOrEmpty(inputName))
        {
            query = query.Where(u =>
                u.Name.IndexOf(inputName, StringComparison.OrdinalIgnoreCase) >= 0);
        }

        // 年齢が指定以上
        if (inputAge.HasValue)
        {
            query = query.Where(u => u.Age >= inputAge.Value);
        }

        // 結果の表示
        foreach (var user in query)
        {
            Console.WriteLine($"{user.Name}, {user.Age}");
        }
    }
}
Charlie, 35

この例では、名前に「a」を含み、かつ年齢が30歳のユーザーを検索しています。

AsQueryableを使うことで、条件を動的に追加しやすくしています。

複数リストのJoinと条件分岐

複数のリストを結合(Join)し、条件に応じてフィルタリングする例です。

例えば、注文と顧客のリストを結合し、特定の地域の顧客の注文だけを抽出します。

using System;
using System.Collections.Generic;
using System.Linq;
class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Region { get; set; }
}
class Order
{
    public int Id { get; set; }
    public int CustomerId { get; set; }
    public string Product { get; set; }
}
class Program
{
    static void Main()
    {
        var customers = new List<Customer>
        {
            new Customer { Id = 1, Name = "Alice", Region = "Tokyo" },
            new Customer { Id = 2, Name = "Bob", Region = "Osaka" }
        };
        var orders = new List<Order>
        {
            new Order { Id = 101, CustomerId = 1, Product = "Laptop" },
            new Order { Id = 102, CustomerId = 2, Product = "Tablet" },
            new Order { Id = 103, CustomerId = 1, Product = "Smartphone" }
        };
        string targetRegion = "Tokyo";
        var query = from o in orders
                    join c in customers on o.CustomerId equals c.Id
                    where c.Region == targetRegion
                    select new
                    {
                        CustomerName = c.Name,
                        o.Product
                    };
        foreach (var item in query)
        {
            Console.WriteLine($"{item.CustomerName} ordered {item.Product}");
        }
    }
}
Alice ordered Laptop
Alice ordered Smartphone

この例では、Tokyo地域の顧客の注文だけを抽出しています。

join句とwhere句を組み合わせて柔軟に条件分岐が可能です。

よくある落とし穴とデバッグポイント

実行時のSQLに現れない条件

Entity FrameworkなどのORMを使ったLINQクエリでは、LINQ式がSQLに変換されてデータベースに送信されます。

しかし、すべてのC#コードがSQLに変換されるわけではなく、一部の条件やメソッドはクライアント側で評価されることがあります。

例えば、LINQの中でカスタムメソッドや.NETの一部の関数を使うと、それらはSQLに変換できず、クエリの一部としてSQLに現れません。

その結果、SQLの実行時には条件が反映されず、クライアント側でフィルタリングされるため、パフォーマンスが低下したり、意図しない結果になることがあります。

var query = context.Users.Where(u => CustomFilter(u.Name));

このような場合、CustomFilterはSQLに変換されず、全件取得後にメモリ上で評価されます。

SQLログを確認してもCustomFilterに関する条件は見当たりません。

対策としては、SQLに変換可能なメソッドや式のみを使うこと、またはAsEnumerable()で明示的にクライアント側評価を分けることが挙げられます。

デバッグ時のIQueryable評価

IQueryable<T>は遅延実行のため、デバッグ時に変数の中身を直接見ると、クエリの内容が表示されず、単にクエリの情報だけが見えることがあります。

実際のデータはクエリが評価されるまで取得されません。

デバッグで結果を確認したい場合は、ToList()ToArray()を使って即時実行し、結果をメモリに展開してから確認すると良いです。

var list = query.ToList();

また、Visual Studioのウォッチウィンドウやクイックウォッチでquery.Expressionquery.ToString()を使うと、生成されるSQLやクエリの構造を確認できます。

非同期LINQメソッドでの例外伝播

Entity Framework Coreなどで非同期LINQメソッド(ToListAsync(), FirstOrDefaultAsync()など)を使う場合、例外は非同期タスクの中で発生します。

これにより、例外が呼び出し元に伝播するタイミングが異なり、awaitを使わずに結果を取得しようとすると例外が捕捉できないことがあります。

try
{
    var users = context.Users.Where(u => u.Age > 20).ToListAsync();
    // awaitを忘れると例外がここで捕捉されない
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

正しくはawaitを使って非同期処理を待機し、例外を捕捉します。

try
{
    var users = await context.Users.Where(u => u.Age > 20).ToListAsync();
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

非同期メソッドの例外処理はasync/awaitの文脈で行うことが重要です。

シーケンスが空の場合の扱い

LINQでシーケンスが空の場合、First()Single()などのメソッドは例外をスローします。

これに対してFirstOrDefault()SingleOrDefault()はデフォルト値(参照型ならnull、値型なら0など)を返します。

var emptyList = new List<int>();
// 例外が発生する
// var first = emptyList.First(); // InvalidOperationException
// デフォルト値を返す
var firstOrDefault = emptyList.FirstOrDefault(); // 0

空のシーケンスを扱う場合は、例外を避けるためにFirstOrDefault()を使うか、Any()で存在確認を行うのが安全です。

if (emptyList.Any())
{
    var first = emptyList.First();
}
else
{
    // 空の場合の処理
}

また、Single()は要素が1つだけ存在することを期待するため、0件または複数件の場合に例外が発生します。

用途に応じて使い分けましょう。

これらのポイントを理解し、適切なメソッドを選択することで、LINQの実行時エラーを防ぎ、安定したコードを書くことができます。

まとめ

この記事では、C#のLINQにおける条件分岐の基本から応用までを詳しく解説しました。

WhereSelectでの条件指定、AnyFirstによる存在確認や取得方法、複数条件の組み合わせ、動的条件の組み立て方、null値の安全な扱い、パフォーマンスを意識した書き方、さらに実践的なサンプルやよくある落とし穴とデバッグのポイントまで幅広く理解できます。

これにより、LINQを使った柔軟で効率的なデータ操作が可能になります。

関連記事

Back to top button
目次へ