LINQ

【C#】LINQで配列操作を一気に効率化!検索・変換・並べ替え・集計のベストプラクティス

C#のLINQを使うと、配列に対する検索・変換・並べ替え・集計などをSQL風に一括記述でき、従来のforループよりコードが短く保守しやすくなります。

さらに遅延実行により必要な要素だけを効率良く取得できるため、メモリと処理時間の両面でメリットが得られます。

目次から探す
  1. LINQとは?
  2. 配列の検索
  3. 抽出と部分取得
  4. データ変換
  5. 並べ替え
  6. 集計
  7. 重複排除
  8. 配列同士の結合
  9. データの分割
  10. コレクション変換
  11. パフォーマンス
  12. Null安全
  13. バージョン別トピック
  14. 実務ユースケース
  15. よくある落とし穴
  16. まとめ

LINQとは?

LINQ(Language Integrated Query)は、C#に組み込まれたデータ操作のための機能です。

配列やリスト、データベースなどのコレクションに対して、検索や変換、並べ替え、集計などの操作を簡潔に記述できます。

LINQを使うことで、複雑なループや条件分岐を減らし、読みやすく保守しやすいコードを書けるようになります。

クエリ構文とメソッド構文

LINQには主に2つの書き方があります。

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

クエリ構文

クエリ構文は、SQLのSELECT文に似た形で記述します。

読みやすく直感的ですが、すべてのLINQ機能を使えるわけではありません。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3, 4, 5 };
        // 2以上の数字を抽出するクエリ構文
        var query = from n in numbers
                    where n >= 2
                    select n;
        foreach (var num in query)
        {
            Console.WriteLine(num);
        }
    }
}
2
3
4
5

この例では、fromで配列numbersの要素をnとして取り出し、whereで条件を指定、selectで結果を選択しています。

メソッド構文

メソッド構文は、WhereSelectなどの拡張メソッドを連結して書きます。

LINQの全機能を使いやすく、ラムダ式と組み合わせて柔軟に記述できます。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3, 4, 5 };
        // 2以上の数字を抽出するメソッド構文
        var result = numbers.Where(n => n >= 2);
        foreach (var num in result)
        {
            Console.WriteLine(num);
        }
    }
}
2
3
4
5

メソッド構文は、ラムダ式で条件や変換を指定できるため、より細かい制御が可能です。

IEnumerableと遅延実行

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

IEnumerable<T>は、要素を順に列挙できるインターフェースで、LINQのクエリは実際に結果を取得するまで処理を遅延させる「遅延実行」の仕組みを持っています。

遅延実行の特徴

遅延実行とは、LINQのクエリを定義した時点では処理が実行されず、foreachなどで結果を列挙するときに初めて処理が行われることです。

これにより、無駄な計算を避け、効率的にデータを扱えます。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3, 4, 5 };
        var query = numbers.Where(n =>
        {
            Console.WriteLine($"Checking {n}");
            return n % 2 == 0;
        });
        Console.WriteLine("クエリ定義完了");
        foreach (var num in query)
        {
            Console.WriteLine($"結果: {num}");
        }
    }
}
クエリ定義完了
Checking 1
Checking 2
結果: 2
Checking 3
Checking 4
結果: 4
Checking 5

この例では、Whereの条件がforeachで列挙するときに初めて評価されていることがわかります。

即時実行メソッド

一方で、ToArrayToListCountなどのメソッドは即時実行され、呼び出した時点で処理が完了します。

遅延実行と即時実行を使い分けることで、パフォーマンスやメモリ使用量を最適化できます。

配列にLINQを適用する利点

配列は固定長で要素の追加や削除ができませんが、LINQを使うことで配列の内容を柔軟に操作できます。

  • 簡潔なコード

複雑なループや条件分岐を減らし、読みやすいコードを書けます。

  • 豊富な操作メソッド

検索、変換、並べ替え、集計、結合、グループ化など多彩な操作が可能です。

  • 遅延実行による効率化

必要なデータだけを効率的に処理でき、パフォーマンス向上に役立ちます。

  • 型安全と統合開発環境のサポート

コンパイル時に型チェックされ、IntelliSenseなどの補完機能が使いやすいです。

これらの利点により、配列を扱う際にLINQを活用すると、コードの保守性や開発効率が大きく向上します。

次のセクションからは、具体的な操作方法をサンプルコードとともに詳しく解説していきます。

配列の検索

条件一致検索

Whereによる複数取得

Whereメソッドは、配列の中から指定した条件に合致する複数の要素を抽出します。

条件はラムダ式で記述し、条件を満たすすべての要素を遅延実行で返します。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 10, 15, 20, 25, 30 };
        // 20以上の数字を抽出
        var filtered = numbers.Where(n => n >= 20);
        Console.WriteLine("20以上の数字:");
        foreach (var num in filtered)
        {
            Console.WriteLine(num);
        }
    }
}
20以上の数字:
20
25
30

このコードでは、numbers配列から20以上の要素をすべて取得しています。

Whereは条件に合う要素を列挙するため、複数の結果が返る場合に適しています。

First / FirstOrDefaultによる先頭取得

Firstメソッドは、条件に合致する最初の要素を取得します。

条件を満たす要素がない場合は例外をスローします。

一方、FirstOrDefaultは条件に合う要素がなければ既定値(参照型ならnull、値型ならdefault)を返します。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        string[] words = { "apple", "banana", "cherry", "date" };
        // 'b'で始まる最初の単語を取得
        var firstWord = words.First(w => w.StartsWith("b"));
        Console.WriteLine($"First: {firstWord}");
        // 'z'で始まる単語を取得(存在しないため例外)
        try
        {
            var noWord = words.First(w => w.StartsWith("z"));
        }
        catch (InvalidOperationException)
        {
            Console.WriteLine("First: 条件に合う要素がありません");
        }
        // 'z'で始まる単語を安全に取得(nullになる)
        var safeWord = words.FirstOrDefault(w => w.StartsWith("z"));
        Console.WriteLine($"FirstOrDefault: {(safeWord ?? "null")}");
    }
}
First: banana
First: 条件に合う要素がありません
FirstOrDefault: null

Firstは必ず要素が存在するときに使い、存在しない可能性がある場合はFirstOrDefaultを使うと安全です。

Any / Allによる存在確認

Anyメソッドは、配列に条件を満たす要素が1つでも存在するかを判定します。

条件を省略すると、配列に要素が1つでもあればtrueを返します。

Allメソッドは、配列のすべての要素が条件を満たすかどうかを判定します。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 2, 4, 6, 8 };
        // 偶数が1つでもあるか
        bool hasEven = numbers.Any(n => n % 2 == 0);
        Console.WriteLine($"偶数が1つでもあります: {hasEven}");
        // すべて偶数か
        bool allEven = numbers.All(n => n % 2 == 0);
        Console.WriteLine($"すべて偶数: {allEven}");
        // 空配列でAnyを使う例
        int[] empty = { };
        Console.WriteLine($"空配列に要素があるか: {empty.Any()}");
    }
}
偶数が1つでもあります: True
すべて偶数: True
空配列に要素があるか: False

Anyは条件に合う要素の存在チェックに便利で、Allは全要素が条件を満たすかの判定に使います。

どちらも高速に判定できるため、条件の有無を簡単に確認できます。

抽出と部分取得

インデックス範囲抽出:SkipとTake

Skipメソッドは、指定した数だけ先頭の要素をスキップし、それ以降の要素を取得します。

一方、Takeメソッドは、先頭から指定した数だけ要素を取得します。

これらを組み合わせることで、配列の一部を簡単に抽出できます。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 10, 20, 30, 40, 50, 60, 70 };
        // 先頭から3つの要素を取得
        var firstThree = numbers.Take(3);
        Console.WriteLine("先頭から3つの要素:");
        foreach (var num in firstThree)
        {
            Console.WriteLine(num);
        }
        // 先頭から3つスキップし、その後の4つを取得
        var middleFour = numbers.Skip(3).Take(4);
        Console.WriteLine("4番目から7番目までの要素:");
        foreach (var num in middleFour)
        {
            Console.WriteLine(num);
        }
    }
}
先頭から3つの要素:
10
20
30
4番目から7番目までの要素:
40
50
60
70

この例では、Takeで先頭3つの要素を取得し、SkipTakeを組み合わせて4番目から7番目までの要素を抽出しています。

SkipTakeはページング処理や部分的なデータ取得に便利です。

インターバル抽出:Whereとラムダ式

Whereメソッドは条件に合う要素を抽出しますが、インデックスを使った抽出も可能です。

Whereのラムダ式は2つの引数を受け取れ、2番目の引数で要素のインデックスを取得できます。

これを利用して、特定の間隔で要素を抽出できます。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        string[] words = { "one", "two", "three", "four", "five", "six" };
        // 偶数番目(0始まり)の要素を抽出
        var evenIndexWords = words.Where((word, index) => index % 2 == 0);
        Console.WriteLine("偶数番目の要素:");
        foreach (var word in evenIndexWords)
        {
            Console.WriteLine(word);
        }
    }
}
偶数番目の要素:
one
three
five

このコードでは、インデックスが偶数の要素だけを抽出しています。

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

安全な要素取得:ElementAtOrDefault

ElementAtOrDefaultメソッドは、指定したインデックスの要素を取得します。

インデックスが範囲外の場合は、参照型ならnull、値型ならdefaultを返すため、例外が発生しません。

安全に要素を取得したい場合に便利です。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 100, 200, 300 };
        // インデックス1の要素を取得(200)
        int second = numbers.ElementAtOrDefault(1);
        Console.WriteLine($"インデックス1の要素: {second}");
        // インデックス5の要素を取得(範囲外なので0)
        int outOfRange = numbers.ElementAtOrDefault(5);
        Console.WriteLine($"インデックス5の要素: {outOfRange}");
    }
}
インデックス1の要素: 200
インデックス5の要素: 0

この例では、存在しないインデックスを指定しても例外が発生せず、値型の既定値である0が返されています。

ElementAtを使うと範囲外で例外が発生するため、例外処理を避けたい場合はElementAtOrDefaultを使うと安全です。

データ変換

要素変換:Select

Selectメソッドは、配列の各要素を指定した変換関数に従って変換し、新しいシーケンスを作成します。

元の配列は変更されず、変換後の結果だけが返されます。

プロパティの射影

配列の要素が複雑なオブジェクトの場合、特定のプロパティだけを抽出して新しいシーケンスを作ることがよくあります。

これを「射影」と呼びます。

using System;
using System.Linq;
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
class Program
{
    static void Main()
    {
        var people = new[]
        {
            new Person { Name = "Alice", Age = 30 },
            new Person { Name = "Bob", Age = 25 },
            new Person { Name = "Charlie", Age = 35 }
        };
        // 名前だけを抽出
        var names = people.Select(p => p.Name);
        Console.WriteLine("名前一覧:");
        foreach (var name in names)
        {
            Console.WriteLine(name);
        }
    }
}
名前一覧:
Alice
Bob
Charlie

この例では、people配列からNameプロパティだけを取り出して新しいシーケンスを作成しています。

匿名型・DTOへの変換

Selectを使うと、複数のプロパティをまとめて匿名型やDTO(データ転送オブジェクト)に変換できます。

匿名型は一時的なデータ構造として便利です。

using System;
using System.Linq;
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Department { get; set; }
}
class PersonDto
{
    public string Name { get; set; }
    public string Department { get; set; }
}
class Program
{
    static void Main()
    {
        var people = new[]
        {
            new Person { Name = "Alice", Age = 30, Department = "HR" },
            new Person { Name = "Bob", Age = 25, Department = "IT" },
            new Person { Name = "Charlie", Age = 35, Department = "HR" }
        };
        // 匿名型への変換
        var anonymous = people.Select(p => new { p.Name, p.Department });
        Console.WriteLine("匿名型で名前と部門を表示:");
        foreach (var item in anonymous)
        {
            Console.WriteLine($"Name: {item.Name}, Department: {item.Department}");
        }
        // DTOへの変換
        var dtos = people.Select(p => new PersonDto { Name = p.Name, Department = p.Department });
        Console.WriteLine("DTOで名前と部門を表示:");
        foreach (var dto in dtos)
        {
            Console.WriteLine($"Name: {dto.Name}, Department: {dto.Department}");
        }
    }
}
匿名型で名前と部門を表示:
Name: Alice, Department: HR
Name: Bob, Department: IT
Name: Charlie, Department: HR
DTOで名前と部門を表示:
Name: Alice, Department: HR
Name: Bob, Department: IT
Name: Charlie, Department: HR

匿名型は簡単に使えますが、メソッドの戻り値やクラス間の受け渡しにはDTOのような明示的な型を使うと安全です。

多次元展開:SelectMany

SelectManyは、各要素がさらにコレクションを持つ場合に、それらを一つの平坦なシーケンスに展開します。

例えば、複数の配列の配列を一つの配列にまとめるときに使います。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        string[][] groups = new[]
        {
            new[] { "Alice", "Bob" },
            new[] { "Charlie", "David" },
            new[] { "Eve" }
        };
        // グループごとの配列を一つの配列に展開
        var allNames = groups.SelectMany(g => g);
        Console.WriteLine("すべての名前:");
        foreach (var name in allNames)
        {
            Console.WriteLine(name);
        }
    }
}
すべての名前:
Alice
Bob
Charlie
David
Eve

この例では、groupsという配列の配列をSelectManyで平坦化し、すべての名前を一つのシーケンスとして取得しています。

Selectだと配列の配列のままですが、SelectManyを使うとネストを解消できます。

並べ替え

昇順:OrderBy

OrderByメソッドは、指定したキーに基づいて配列の要素を昇順に並べ替えます。

元の配列は変更されず、新しい並べ替え済みのシーケンスが返されます。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 30, 10, 50, 20, 40 };
        // 昇順に並べ替え
        var ascending = numbers.OrderBy(n => n);
        Console.WriteLine("昇順に並べ替えた結果:");
        foreach (var num in ascending)
        {
            Console.WriteLine(num);
        }
    }
}
昇順に並べ替えた結果:
10
20
30
40
50

この例では、OrderByにキーとして要素自身を指定し、数値を小さい順に並べ替えています。

降順:OrderByDescending

OrderByDescendingOrderByの逆で、指定したキーに基づいて降順に並べ替えます。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 30, 10, 50, 20, 40 };
        // 降順に並べ替え
        var descending = numbers.OrderByDescending(n => n);
        Console.WriteLine("降順に並べ替えた結果:");
        foreach (var num in descending)
        {
            Console.WriteLine(num);
        }
    }
}
降順に並べ替えた結果:
50
40
30
20
10

降順に並べ替えたい場合はOrderByDescendingを使うと簡単に実現できます。

複数キー:ThenByとThenByDescending

複数のキーで並べ替えたい場合は、まずOrderByまたはOrderByDescendingで一次キーを指定し、その後にThenByThenByDescendingで二次キー以降を指定します。

using System;
using System.Linq;
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
class Program
{
    static void Main()
    {
        var people = new[]
        {
            new Person { Name = "Alice", Age = 30 },
            new Person { Name = "Bob", Age = 25 },
            new Person { Name = "Charlie", Age = 30 },
            new Person { Name = "David", Age = 25 }
        };
        // 年齢で昇順に並べ替え、同じ年齢なら名前で昇順に並べ替え
        var sorted = people.OrderBy(p => p.Age).ThenBy(p => p.Name);
        Console.WriteLine("年齢と名前で並べ替えた結果:");
        foreach (var person in sorted)
        {
            Console.WriteLine($"{person.Name} ({person.Age})");
        }
    }
}
年齢と名前で並べ替えた結果:
Bob (25)
David (25)
Alice (30)
Charlie (30)

この例では、まずAgeで昇順に並べ替え、同じAgeのグループ内でNameを昇順に並べ替えています。

カスタム比較:IComparer利用

独自の並べ替えルールを適用したい場合は、IComparer<T>インターフェースを実装したクラスを作成し、OrderByOrderByDescendingの第2引数に渡すことができます。

using System;
using System.Collections.Generic;
using System.Linq;
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
// 年齢の偶数・奇数で優先順位をつけるカスタム比較
class EvenOddAgeComparer : IComparer<int>
{
    public int Compare(int x, int y)
    {
        // 偶数を先に、奇数を後に並べる
        bool xIsEven = x % 2 == 0;
        bool yIsEven = y % 2 == 0;
        if (xIsEven && !yIsEven) return -1;
        if (!xIsEven && yIsEven) return 1;
        // 両方偶数または両方奇数なら通常の昇順
        return x.CompareTo(y);
    }
}
class Program
{
    static void Main()
    {
        var people = new[]
        {
            new Person { Name = "Alice", Age = 31 },
            new Person { Name = "Bob", Age = 24 },
            new Person { Name = "Charlie", Age = 29 },
            new Person { Name = "David", Age = 22 }
        };
        var comparer = new EvenOddAgeComparer();
        // カスタム比較で並べ替え
        var sorted = people.OrderBy(p => p.Age, comparer);
        Console.WriteLine("カスタム比較で並べ替えた結果:");
        foreach (var person in sorted)
        {
            Console.WriteLine($"{person.Name} ({person.Age})");
        }
    }
}
カスタム比較で並べ替えた結果:
David (22)
Bob (24)
Charlie (29)
Alice (31)

この例では、年齢が偶数の人を先に、奇数の人を後に並べ替えています。

IComparerを使うと、標準の昇順・降順以外の複雑な並べ替えも実現できます。

集計

基本統計メソッド

LINQには配列やコレクションの要素数や最小値、最大値、合計、平均を求める便利なメソッドが用意されています。

Count / LongCount

Countは要素数を取得します。

LongCountCountと同様ですが、要素数が非常に多い場合に64ビット整数で返すため、巨大なコレクションに適しています。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3, 4, 5 };
        int count = numbers.Count();
        long longCount = numbers.LongCount();
        Console.WriteLine($"Count: {count}");
        Console.WriteLine($"LongCount: {longCount}");
    }
}
Count: 5
LongCount: 5

Min / Max

Minは最小値、Maxは最大値を取得します。

数値だけでなく、文字列や日付など比較可能な型にも使えます。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 10, 20, 5, 30, 15 };
        int min = numbers.Min();
        int max = numbers.Max();
        Console.WriteLine($"最小値: {min}");
        Console.WriteLine($"最大値: {max}");
    }
}
最小値: 5
最大値: 30

Sum / Average

Sumは要素の合計値を、Averageは平均値を計算します。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 10, 20, 30, 40 };
        int sum = numbers.Sum();
        double average = numbers.Average();
        Console.WriteLine($"合計: {sum}");
        Console.WriteLine($"平均: {average}");
    }
}
合計: 100
平均: 25

条件付き集計

条件を満たす要素だけを集計したい場合は、Whereで絞り込んでから集計メソッドを使います。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 10, 15, 20, 25, 30 };
        // 20以上の要素の合計
        int sumOver20 = numbers.Where(n => n >= 20).Sum();
        // 20未満の要素の平均
        double avgUnder20 = numbers.Where(n => n < 20).Average();
        Console.WriteLine($"20以上の合計: {sumOver20}");
        Console.WriteLine($"20未満の平均: {avgUnder20}");
    }
}
20以上の合計: 75
20未満の平均: 12.5

グループ集計:GroupBy

GroupByは指定したキーで要素をグループ化し、グループごとに集計や処理ができます。

using System;
using System.Linq;
class Person
{
    public string Department { get; set; }
    public int Salary { get; set; }
}
class Program
{
    static void Main()
    {
        var employees = new[]
        {
            new Person { Department = "HR", Salary = 50000 },
            new Person { Department = "IT", Salary = 70000 },
            new Person { Department = "HR", Salary = 55000 },
            new Person { Department = "IT", Salary = 72000 },
            new Person { Department = "Sales", Salary = 45000 }
        };
        // 部門ごとにグループ化
        var grouped = employees.GroupBy(e => e.Department);
        foreach (var group in grouped)
        {
            Console.WriteLine($"部門: {group.Key}");
            foreach (var emp in group)
            {
                Console.WriteLine($"  給与: {emp.Salary}");
            }
        }
    }
}
部門: HR
  給与: 50000
  給与: 55000
部門: IT
  給与: 70000
  給与: 72000
部門: Sales
  給与: 45000

結果成形:Selectと匿名型

グループ化した結果に対して、集計値を計算し、Selectで匿名型などに成形することが多いです。

using System;
using System.Linq;
class Person
{
    public string Department { get; set; }
    public int Salary { get; set; }
}
class Program
{
    static void Main()
    {
        var employees = new[]
        {
            new Person { Department = "HR", Salary = 50000 },
            new Person { Department = "IT", Salary = 70000 },
            new Person { Department = "HR", Salary = 55000 },
            new Person { Department = "IT", Salary = 72000 },
            new Person { Department = "Sales", Salary = 45000 }
        };
        // 部門ごとに給与の平均を計算し匿名型で成形
        var avgSalaries = employees
            .GroupBy(e => e.Department)
            .Select(g => new
            {
                Department = g.Key,
                AverageSalary = g.Average(e => e.Salary),
                Count = g.Count()
            });
        foreach (var item in avgSalaries)
        {
            Console.WriteLine($"部門: {item.Department}, 平均給与: {item.AverageSalary}, 人数: {item.Count}");
        }
    }
}
部門: HR, 平均給与: 52500, 人数: 2
部門: IT, 平均給与: 71000, 人数: 2
部門: Sales, 平均給与: 45000, 人数: 1

このようにGroupBySelectを組み合わせることで、グループごとの集計結果を簡単に取得できます。

重複排除

標準重複排除:Distinct

Distinctメソッドは、配列やコレクション内の重複する要素を取り除き、一意の要素だけを返します。

元の配列は変更されず、新しいシーケンスが返されます。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 10, 20, 10, 30, 20, 40 };
        // 重複を除外
        var distinctNumbers = numbers.Distinct();
        Console.WriteLine("重複を除外した結果:");
        foreach (var num in distinctNumbers)
        {
            Console.WriteLine(num);
        }
    }
}
重複を除外した結果:
10
20
30
40

Distinctは要素の型がプリミティブ型やEqualsGetHashCodeが適切に実装されている型に対して有効です。

キー指定重複排除:DistinctBy

DistinctByは.NET 6以降で利用可能なメソッドで、要素の特定のキーを基準に重複を排除します。

オブジェクトの特定のプロパティで重複を判定したい場合に便利です。

using System;
using System.Linq;
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
class Program
{
    static void Main()
    {
        var people = new[]
        {
            new Person { Name = "Alice", Age = 30 },
            new Person { Name = "Bob", Age = 25 },
            new Person { Name = "Alice", Age = 35 },
            new Person { Name = "Charlie", Age = 25 }
        };
        // 名前で重複を排除(最初に出現した要素を残す)
        var distinctByName = people.DistinctBy(p => p.Name);
        Console.WriteLine("名前で重複排除した結果:");
        foreach (var person in distinctByName)
        {
            Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
        }
    }
}
名前で重複排除した結果:
Name: Alice, Age: 30
Name: Bob, Age: 25
Name: Charlie, Age: 25

この例では、Nameプロパティをキーにして重複を排除しています。

DistinctByはキーの重複を判定し、最初に出現した要素を残します。

複数のプロパティを組み合わせて重複判定したい場合は匿名型をキーに指定することも可能です。

配列同士の結合

連結:ConcatとUnion

Concatメソッドは、2つの配列やコレクションを単純に連結して一つのシーケンスにします。

重複は排除されず、元の順序が保たれます。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbersA = { 1, 2, 3 };
        int[] numbersB = { 3, 4, 5 };
        // 配列を連結
        var concatenated = numbersA.Concat(numbersB);
        Console.WriteLine("Concatの結果:");
        foreach (var num in concatenated)
        {
            Console.WriteLine(num);
        }
    }
}
Concatの結果:
1
2
3
3
4
5

一方、Unionメソッドは2つの配列の要素を結合し、重複を除外して一意の要素だけを返します。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbersA = { 1, 2, 3 };
        int[] numbersB = { 3, 4, 5 };
        // 配列を結合し重複を除外
        var unioned = numbersA.Union(numbersB);
        Console.WriteLine("Unionの結果:");
        foreach (var num in unioned)
        {
            Console.WriteLine(num);
        }
    }
}
Unionの結果:
1
2
3
4
5

内部結合:Join

Joinメソッドは、2つの配列を指定したキーで内部結合(SQLのINNER JOINに相当)します。

結合条件に合致する要素の組み合わせを新しいシーケンスとして返します。

using System;
using System.Linq;
class Employee
{
    public int EmployeeId { get; set; }
    public string Name { get; set; }
    public int DepartmentId { get; set; }
}
class Department
{
    public int DepartmentId { get; set; }
    public string DepartmentName { get; set; }
}
class Program
{
    static void Main()
    {
        var employees = new[]
        {
            new Employee { EmployeeId = 1, Name = "Alice", DepartmentId = 1 },
            new Employee { EmployeeId = 2, Name = "Bob", DepartmentId = 2 },
            new Employee { EmployeeId = 3, Name = "Charlie", DepartmentId = 1 }
        };
        var departments = new[]
        {
            new Department { DepartmentId = 1, DepartmentName = "HR" },
            new Department { DepartmentId = 2, DepartmentName = "IT" }
        };
        // 内部結合
        var query = employees.Join(
            departments,
            e => e.DepartmentId,
            d => d.DepartmentId,
            (e, d) => new { e.Name, d.DepartmentName }
        );
        Console.WriteLine("内部結合の結果:");
        foreach (var item in query)
        {
            Console.WriteLine($"Name: {item.Name}, Department: {item.DepartmentName}");
        }
    }
}
内部結合の結果:
Name: Alice, Department: HR
Name: Bob, Department: IT
Name: Charlie, Department: HR

左外部結合:GroupJoinとDefaultIfEmpty

GroupJoinは、左側の配列の各要素に対して右側の配列の関連する要素をグループ化して結合します。

これにDefaultIfEmptyを組み合わせることで、右側に対応する要素がない場合でも左側の要素を保持する「左外部結合(LEFT OUTER JOIN)」を実現できます。

using System;
using System.Linq;
class Employee
{
    public int EmployeeId { get; set; }
    public string Name { get; set; }
    public int DepartmentId { get; set; }
}
class Department
{
    public int DepartmentId { get; set; }
    public string DepartmentName { get; set; }
}
class Program
{
    static void Main()
    {
        var employees = new[]
        {
            new Employee { EmployeeId = 1, Name = "Alice", DepartmentId = 1 },
            new Employee { EmployeeId = 2, Name = "Bob", DepartmentId = 2 },
            new Employee { EmployeeId = 3, Name = "Charlie", DepartmentId = 3 } // 部門なし
        };
        var departments = new[]
        {
            new Department { DepartmentId = 1, DepartmentName = "HR" },
            new Department { DepartmentId = 2, DepartmentName = "IT" }
        };
        // 左外部結合
        var query = employees.GroupJoin(
            departments,
            e => e.DepartmentId,
            d => d.DepartmentId,
            (e, deptGroup) => new { Employee = e, Departments = deptGroup.DefaultIfEmpty() }
        )
        .SelectMany(
            x => x.Departments,
            (x, d) => new
            {
                Name = x.Employee.Name,
                DepartmentName = d?.DepartmentName ?? "未所属"
            }
        );
        Console.WriteLine("左外部結合の結果:");
        foreach (var item in query)
        {
            Console.WriteLine($"Name: {item.Name}, Department: {item.DepartmentName}");
        }
    }
}
左外部結合の結果:
Name: Alice, Department: HR
Name: Bob, Department: IT
Name: Charlie, Department: 未所属

この例では、Charlieさんは部門に所属していませんが、左外部結合により社員情報は保持され、部門名は「未所属」と表示されています。

データの分割

チャンク化:Chunk

Chunkメソッドは、配列やコレクションを指定したサイズごとに分割し、複数の小さな配列のシーケンスとして返します。

大量のデータを扱う際に、処理単位を分割したい場合に便利です。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = Enumerable.Range(1, 10).ToArray();
        // 3つずつのチャンクに分割
        var chunks = numbers.Chunk(3);
        int chunkIndex = 1;
        foreach (var chunk in chunks)
        {
            Console.WriteLine($"チャンク {chunkIndex}:");
            foreach (var num in chunk)
            {
                Console.Write($"{num} ");
            }
            Console.WriteLine();
            chunkIndex++;
        }
    }
}
チャンク 1:
1 2 3
チャンク 2:
4 5 6
チャンク 3:
7 8 9
チャンク 4:
10

この例では、1から10までの数字を3つずつのグループに分割しています。

最後のチャンクは要素数が3未満でも問題なく処理されます。

ページング:SkipとTake

ページング処理は、大量のデータをページ単位で表示したい場合に使います。

Skipで先頭から指定数の要素をスキップし、Takeでページサイズ分の要素を取得します。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = Enumerable.Range(1, 20).ToArray();
        int pageSize = 5;
        int pageNumber = 3; // 3ページ目を取得
        var page = numbers.Skip((pageNumber - 1) * pageSize).Take(pageSize);
        Console.WriteLine($"{pageNumber}ページ目のデータ:");
        foreach (var num in page)
        {
            Console.WriteLine(num);
        }
    }
}
3ページ目のデータ:
11
12
13
14
15

このコードでは、20個の数字から3ページ目(11~15番目の要素)を取得しています。

SkipTakeを組み合わせることで簡単にページングが実装できます。

コレクション変換

配列化:ToArray

ToArrayメソッドは、LINQのクエリ結果や任意のIEnumerable<T>を配列に変換します。

元のコレクションの内容をコピーして新しい配列を作成するため、配列としての高速なアクセスが必要な場合に使います。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = Enumerable.Range(1, 5);
        // IEnumerableから配列に変換
        int[] array = numbers.ToArray();
        Console.WriteLine("配列の要素:");
        foreach (var num in array)
        {
            Console.WriteLine(num);
        }
    }
}
配列の要素:
1
2
3
4
5

リスト化:ToList

ToListメソッドは、IEnumerable<T>List<T>に変換します。

リストは可変長で要素の追加や削除が可能なため、動的な操作が必要な場合に便利です。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = Enumerable.Range(1, 5);
        // IEnumerableからリストに変換
        List<int> list = numbers.ToList();
        Console.WriteLine("リストの要素:");
        foreach (var num in list)
        {
            Console.WriteLine(num);
        }
    }
}
リストの要素:
1
2
3
4
5

Dictionary化:ToDictionary

ToDictionaryメソッドは、IEnumerable<T>の要素をキーと値に変換してDictionary<TKey, TValue>を作成します。

キーの重複があると例外が発生するため、キーが一意であることが前提です。

using System;
using System.Collections.Generic;
using System.Linq;
class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
}
class Program
{
    static void Main()
    {
        var people = new[]
        {
            new Person { Id = 1, Name = "Alice" },
            new Person { Id = 2, Name = "Bob" },
            new Person { Id = 3, Name = "Charlie" }
        };
        // Idをキー、Nameを値にしてDictionaryを作成
        Dictionary<int, string> dict = people.ToDictionary(p => p.Id, p => p.Name);
        Console.WriteLine("Dictionaryの内容:");
        foreach (var kvp in dict)
        {
            Console.WriteLine($"Key: {kvp.Key}, Value: {kvp.Value}");
        }
    }
}
Dictionaryの内容:
Key: 1, Value: Alice
Key: 2, Value: Bob
Key: 3, Value: Charlie

Set化:ToHashSet

ToHashSetメソッドは、IEnumerable<T>HashSet<T>に変換します。

HashSetは重複を許さず、高速な検索が可能なコレクションです。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = new[] { 1, 2, 2, 3, 4, 4, 5 };
        // 重複を排除してHashSetに変換
        HashSet<int> set = numbers.ToHashSet();
        Console.WriteLine("HashSetの要素:");
        foreach (var num in set)
        {
            Console.WriteLine(num);
        }
    }
}
HashSetの要素:
1
2
3
4
5

ToHashSetは重複排除と高速な検索が必要な場合に便利です。

パフォーマンス

遅延実行と即時実行のトレードオフ

LINQの多くのメソッドは遅延実行を採用しており、クエリの定義時には処理が実行されず、結果を列挙するときに初めて処理が行われます。

これにより、不要な計算を避けて効率的にデータを扱えますが、一方で何度も列挙すると同じ処理が繰り返されるためパフォーマンスに影響が出ることがあります。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3, 4, 5 };
        var query = numbers.Where(n =>
        {
            Console.WriteLine($"Evaluating {n}");
            return n % 2 == 0;
        });
        Console.WriteLine("クエリ定義完了");
        // 1回目の列挙
        foreach (var num in query)
        {
            Console.WriteLine($"結果: {num}");
        }
        // 2回目の列挙(再度評価される)
        foreach (var num in query)
        {
            Console.WriteLine($"結果: {num}");
        }
    }
}
クエリ定義完了
Evaluating 1
Evaluating 2
結果: 2
Evaluating 3
Evaluating 4
結果: 4
Evaluating 5
Evaluating 1
Evaluating 2
結果: 2
Evaluating 3
Evaluating 4
結果: 4
Evaluating 5

このように遅延実行は便利ですが、複数回列挙すると処理が繰り返されるため、結果を使い回す場合は即時実行メソッドToArrayToListで一度評価しておくことが推奨されます。

複数列挙の回避

LINQクエリを複数回列挙すると、毎回同じ処理が実行されてしまい、パフォーマンスが低下します。

これを避けるために、結果を一時的に保存して使い回す方法があります。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3, 4, 5 };
        var query = numbers.Where(n => n % 2 == 0);
        // ToListで即時実行し結果を保存
        var cached = query.ToList();
        Console.WriteLine("1回目の列挙:");
        foreach (var num in cached)
        {
            Console.WriteLine(num);
        }
        Console.WriteLine("2回目の列挙:");
        foreach (var num in cached)
        {
            Console.WriteLine(num);
        }
    }
}
1回目の列挙:
2
4
2回目の列挙:
2
4

ToListToArrayで結果をキャッシュすることで、複数回の列挙時に再評価を防ぎ、パフォーマンスを向上させられます。

バッファリングによる最適化

LINQの遅延実行は便利ですが、複雑なクエリや大規模データではパフォーマンスに影響が出ることがあります。

バッファリングとは、一度にデータをまとめて取得・保存し、繰り返しの処理を減らす手法です。

例えば、ToListToArrayはバッファリングを行い、元のシーケンスを一度評価してメモリに保持します。

これにより、後続の処理が高速化されます。

また、GroupByOrderByなどのメソッドも内部でバッファリングを行い、効率的に処理を実現しています。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = Enumerable.Range(1, 1000000);
        // 遅延実行のまま複数回列挙するとパフォーマンス低下
        var query = numbers.Where(n => n % 2 == 0);
        // バッファリングして一度だけ評価
        var buffered = query.ToList();
        Console.WriteLine($"偶数の数: {buffered.Count}");
    }
}

大量データを扱う場合は、必要に応じてバッファリングを行い、無駄な再評価を防ぐことが重要です。

ただし、バッファリングはメモリ使用量が増えるため、状況に応じて使い分けてください。

Null安全

Null要素の扱い

LINQを使う際に配列やコレクション内にnull要素が含まれている場合、操作によっては例外が発生したり、意図しない結果になることがあります。

特に、プロパティアクセスやメソッド呼び出しを行うラムダ式内でnullを参照するとNullReferenceExceptionが発生するため注意が必要です。

using System;
using System.Linq;
class Person
{
    public string Name { get; set; }
}
class Program
{
    static void Main()
    {
        Person[] people = {
            new Person { Name = "Alice" },
            null,
            new Person { Name = "Bob" }
        };
        // nullチェックを入れずにNameを取得しようとすると例外が発生する
        try
        {
            var names = people.Select(p => p.Name);
            foreach (var name in names)
            {
                Console.WriteLine(name);
            }
        }
        catch (NullReferenceException)
        {
            Console.WriteLine("NullReferenceExceptionが発生しました");
        }
        // nullチェックを入れて安全に処理する
        var safeNames = people
            .Where(p => p != null)
            .Select(p => p.Name);
        Console.WriteLine("nullチェック後の名前一覧:");
        foreach (var name in safeNames)
        {
            Console.WriteLine(name);
        }
    }
}
NullReferenceExceptionが発生しました
nullチェック後の名前一覧:
Alice
Bob

このように、null要素が混在する可能性がある場合は、Wherenullを除外するか、ラムダ式内でp?.Nameのように安全呼び出し演算子を使うなどの対策が必要です。

空シーケンスへの安全な集計

LINQの集計メソッド(MinMaxAverageなど)は、空のシーケンスに対して呼び出すと例外をスローします。

空の配列やフィルタリング結果が空になる可能性がある場合は、事前に要素数をチェックするか、例外を回避する方法を使う必要があります。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { };
        // 空シーケンスに対してMinを呼ぶと例外が発生する
        try
        {
            int min = numbers.Min();
            Console.WriteLine($"最小値: {min}");
        }
        catch (InvalidOperationException)
        {
            Console.WriteLine("空のシーケンスに対してMinを呼びました");
        }
        // 空でない場合のみ集計を行う
        if (numbers.Any())
        {
            int min = numbers.Min();
            Console.WriteLine($"最小値: {min}");
        }
        else
        {
            Console.WriteLine("シーケンスが空のため集計をスキップしました");
        }
        // DefaultIfEmptyを使って既定値を設定する方法
        int minOrDefault = numbers.DefaultIfEmpty(0).Min();
        Console.WriteLine($"DefaultIfEmptyを使った最小値: {minOrDefault}");
    }
}
空のシーケンスに対してMinを呼びました
シーケンスが空のため集計をスキップしました
DefaultIfEmptyを使った最小値: 0

DefaultIfEmptyは空のシーケンスに既定値を挿入するため、集計メソッドの例外を防げます。

空シーケンスを扱う際は、これらの方法で安全に処理することが重要です。

バージョン別トピック

C# 9以前との互換

C# 9以前のバージョンでもLINQの基本的な機能は利用可能ですが、一部の新しいメソッドや構文は使えません。

例えば、DistinctByChunkなどのメソッドは.NET 6以降で追加されたため、C# 9以前の環境では利用できません。

また、C# 9以前ではレコード型やパターンマッチングの強化がなかったため、匿名型やDTOの扱い方やLINQクエリの記述に若干の違いがあります。

// C# 9以前でも使える基本的なLINQ例
using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3, 4, 5 };
        // WhereとSelectは古いバージョンでも利用可能
        var evenSquares = numbers.Where(n => n % 2 == 0).Select(n => n * n);
        foreach (var num in evenSquares)
        {
            Console.WriteLine(num);
        }
    }
}
4
16

このように、基本的なLINQ操作はC# 9以前でも問題なく動作しますが、新機能は使えないため注意が必要です。

.NET 6以降の追加メソッド

.NET 6以降ではLINQに多くの便利な拡張メソッドが追加され、配列やコレクション操作がさらに効率的になりました。

代表的な追加メソッドには以下があります。

  • Chunk(int size):配列やコレクションを指定サイズのチャンクに分割します
  • DistinctBy<TKey>(Func<T, TKey> keySelector):指定したキーで重複を排除します
  • TryGetNonEnumeratedCount(out int count):列挙せずに要素数を取得できるか判定します
  • MaxBy / MinBy:指定したキーに基づいて最大・最小の要素を取得します
using System;
using System.Linq;
class Program
{
    static void Main()
    {
        int[] numbers = Enumerable.Range(1, 10).ToArray();
        // Chunkで3つずつに分割
        var chunks = numbers.Chunk(3);
        Console.WriteLine("Chunkの結果:");
        foreach (var chunk in chunks)
        {
            Console.WriteLine(string.Join(", ", chunk));
        }
        // DistinctByでキー指定の重複排除
        var people = new[]
        {
            new { Name = "Alice", Age = 30 },
            new { Name = "Bob", Age = 25 },
            new { Name = "Alice", Age = 35 }
        };
        var distinctByName = people.DistinctBy(p => p.Name);
        Console.WriteLine("DistinctByの結果:");
        foreach (var person in distinctByName)
        {
            Console.WriteLine($"{person.Name} ({person.Age})");
        }
    }
}
Chunkの結果:
1, 2, 3
4, 5, 6
7, 8, 9
10
DistinctByの結果:
Alice (30)
Bob (25)

これらの新メソッドはコードの簡潔化やパフォーマンス向上に役立ちます。

System.Linq拡張ライブラリ

標準のLINQに加えて、System.Linq名前空間には多くの拡張メソッドが用意されています。

さらに、コミュニティやMicrosoftが提供する拡張ライブラリを利用することで、LINQの機能を拡張できます。

代表的な拡張ライブラリには以下があります。

  • MoreLINQ:より多彩なLINQ操作を提供するオープンソースライブラリ。BatchDistinctByのようなメソッドを.NET標準より早く提供していました
  • System.Interactive (Ix.NET):リアクティブプログラミング向けのLINQ拡張を含み、非同期やイベントストリームの操作に強みがあります
  • LanguageExt:関数型プログラミングの概念をC#に導入し、LINQと組み合わせて使える多彩なデータ構造や演算子を提供します

これらのライブラリを活用すると、標準LINQでは実現しにくい複雑なデータ操作やパフォーマンス最適化が可能になります。

プロジェクトの要件に応じて検討すると良いでしょう。

実務ユースケース

データ取得層での配列加工

データ取得層では、データベースや外部APIから取得した配列データを必要な形に加工することが多いです。

LINQを使うと、不要なデータのフィルタリングや特定のプロパティの抽出、並べ替えなどを簡潔に行えます。

using System;
using System.Linq;
class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public bool IsAvailable { get; set; }
}
class Program
{
    static void Main()
    {
        var products = new[]
        {
            new Product { Id = 1, Name = "Laptop", Price = 1200m, IsAvailable = true },
            new Product { Id = 2, Name = "Tablet", Price = 300m, IsAvailable = false },
            new Product { Id = 3, Name = "Smartphone", Price = 800m, IsAvailable = true }
        };
        // 利用可能な商品のみ抽出し、価格の高い順に並べ替え、名前だけを取得
        var availableProductNames = products
            .Where(p => p.IsAvailable)
            .OrderByDescending(p => p.Price)
            .Select(p => p.Name)
            .ToArray();
        Console.WriteLine("利用可能な商品の名前(価格の高い順):");
        foreach (var name in availableProductNames)
        {
            Console.WriteLine(name);
        }
    }
}
利用可能な商品の名前(価格の高い順):
Laptop
Smartphone

このように、データ取得層でLINQを活用すると、必要なデータだけを効率的に抽出・整形できます。

ログデータのリアルタイム集計

ログデータのリアルタイム集計では、配列やリストに蓄積されたログ情報から特定の条件で集計を行い、状況を把握します。

LINQの集計メソッドやグループ化を使うと、簡単に集計処理が実装できます。

using System;
using System.Linq;
class LogEntry
{
    public DateTime Timestamp { get; set; }
    public string Level { get; set; } // "Info", "Warning", "Error"
    public string Message { get; set; }
}
class Program
{
    static void Main()
    {
        var logs = new[]
        {
            new LogEntry { Timestamp = DateTime.Now.AddMinutes(-10), Level = "Info", Message = "Start process" },
            new LogEntry { Timestamp = DateTime.Now.AddMinutes(-5), Level = "Warning", Message = "Low disk space" },
            new LogEntry { Timestamp = DateTime.Now.AddMinutes(-2), Level = "Error", Message = "Failed to save file" },
            new LogEntry { Timestamp = DateTime.Now.AddMinutes(-1), Level = "Info", Message = "Process completed" }
        };
        // レベルごとにログ件数を集計
        var logCounts = logs
            .GroupBy(log => log.Level)
            .Select(g => new { Level = g.Key, Count = g.Count() });
        Console.WriteLine("ログレベルごとの件数:");
        foreach (var item in logCounts)
        {
            Console.WriteLine($"{item.Level}: {item.Count}件");
        }
    }
}
ログレベルごとの件数:
Info: 2件
Warning: 1件
Error: 1件

リアルタイムでログを集計し、問題の早期発見や監視に役立てられます。

UIバインディング前の整形

UIにデータを表示する際は、配列のままではなく、必要な情報だけを抽出したり、並べ替えやグループ化を行って見やすく整形することが多いです。

LINQを使うと、UIバインディングに適した形に簡単に変換できます。

using System;
using System.Linq;
class Order
{
    public int OrderId { get; set; }
    public string Customer { get; set; }
    public decimal Amount { get; set; }
    public DateTime OrderDate { get; set; }
}
class Program
{
    static void Main()
    {
        var orders = new[]
        {
            new Order { OrderId = 1, Customer = "Alice", Amount = 250m, OrderDate = new DateTime(2023, 5, 1) },
            new Order { OrderId = 2, Customer = "Bob", Amount = 150m, OrderDate = new DateTime(2023, 5, 3) },
            new Order { OrderId = 3, Customer = "Alice", Amount = 300m, OrderDate = new DateTime(2023, 5, 2) }
        };
        // 顧客ごとに注文をグループ化し、合計金額を計算
        var summary = orders
            .GroupBy(o => o.Customer)
            .Select(g => new
            {
                Customer = g.Key,
                TotalAmount = g.Sum(o => o.Amount),
                Orders = g.OrderBy(o => o.OrderDate).ToList()
            })
            .OrderByDescending(s => s.TotalAmount)
            .ToList();
        Console.WriteLine("顧客別注文サマリー:");
        foreach (var item in summary)
        {
            Console.WriteLine($"顧客: {item.Customer}, 合計金額: {item.TotalAmount}円");
            foreach (var order in item.Orders)
            {
                Console.WriteLine($"  注文ID: {order.OrderId}, 日付: {order.OrderDate:d}, 金額: {order.Amount}円");
            }
        }
    }
}
顧客別注文サマリー:
顧客: Alice, 合計金額: 550円
  注文ID: 1, 日付: 2023/05/01, 金額: 250円
  注文ID: 3, 日付: 2023/05/02, 金額: 300円
顧客: Bob, 合計金額: 150円
  注文ID: 2, 日付: 2023/05/03, 金額: 150円

このように、UIに表示しやすい形に整形してからバインディングすることで、ユーザーにとって見やすく使いやすい画面を実現できます。

よくある落とし穴

パフォーマンスが出ないパターン

LINQは便利ですが、使い方によってはパフォーマンスが低下することがあります。

特に以下のようなパターンに注意が必要です。

  • 複数回の列挙

遅延実行のクエリを複数回列挙すると、毎回同じ処理が繰り返されてしまいます。

大量データの場合はToListToArrayで一度評価してキャッシュすることが重要です。

  • 不要な中間コレクションの生成

WhereSelectを連続して使うと中間シーケンスが複数生成されます。

LINQは遅延実行なので大きな問題にはなりにくいですが、複雑なクエリではパフォーマンスに影響することがあります。

  • 大規模データでの無駄な全件処理

全件を処理するクエリは大規模データで時間がかかります。

必要な部分だけをSkipTakeで絞り込むなど工夫が必要です。

// 複数回列挙の例(非効率)
var query = numbers.Where(n => n % 2 == 0);
int count = query.Count();  // ここで列挙
var list = query.ToList();  // ここで再度列挙

副作用を含むラムダ式

LINQのラムダ式は純粋関数として使うのが望ましいですが、副作用を含む処理を行うと予期せぬ動作やバグの原因になります。

例えば、状態を変更したり、外部リソースにアクセスする処理は避けるべきです。

int sum = 0;
var numbers = new[] { 1, 2, 3 };
// 副作用ありのラムダ式(推奨されない)
var evens = numbers.Where(n =>
{
    sum += n;  // 副作用
    return n % 2 == 0;
});
foreach (var n in evens)
{
    Console.WriteLine(n);
}
Console.WriteLine($"合計: {sum}");  // sumの値が予想外になる可能性あり

副作用があると、遅延実行のタイミングや列挙回数によって結果が変わるため、デバッグが難しくなります。

副作用は避け、必要な場合は明示的に処理を分けましょう。

配列内容を変更できない誤解

LINQは配列やコレクションの内容を変更するものではなく、新しいシーケンスを返す操作です。

元の配列の要素を直接変更したい場合、LINQは適していません。

int[] numbers = { 1, 2, 3 };
// LINQで要素を2倍にした新しいシーケンスを作成
var doubled = numbers.Select(n => n * 2);
// 元の配列は変更されない
Console.WriteLine("元の配列:");
foreach (var n in numbers)
{
    Console.WriteLine(n);
}
Console.WriteLine("変換後のシーケンス:");
foreach (var n in doubled)
{
    Console.WriteLine(n);
}
元の配列:
1
2
3
変換後のシーケンス:
2
4
6

元の配列を直接変更したい場合は、forループやArrayのメソッドを使う必要があります。

LINQはあくまでデータの変換や抽出に使うものであることを理解しましょう。

まとめ

この記事では、C#のLINQを使った配列操作の基本から応用までを詳しく解説しました。

検索や抽出、変換、並べ替え、集計、結合、分割、重複排除など、多彩な操作をサンプルコードとともに紹介しています。

遅延実行の仕組みやパフォーマンスの注意点、Null安全の対策も理解でき、実務での活用例も具体的に示しました。

LINQを効果的に使うことで、配列操作が簡潔かつ効率的になり、保守性の高いコードが書けるようになります。

関連記事

Back to top button