C#を使用したアプリケーション開発において、配列(Array)から特定の条件に合致する要素を検索する操作は、極めて頻繁に発生するタスクの1つです。

単純な値の存在確認から、複雑なオブジェクトのプロパティに基づいた抽出、さらにはパフォーマンスが要求される大規模データ処理まで、その用途は多岐にわたります。

C#には、伝統的なArrayクラスのメソッドに加え、強力なLINQ(Language Integrated Query)が用意されており、状況に応じて最適な手法を選択することが重要です。

本記事では、初学者から実務レベルのエンジニアまで役立つ、C#での配列検索の決定版ガイドとして、各手法の具体的な使い方と使い分けを詳しく解説します。

配列検索の基本メソッド

C#のSystem.Arrayクラスには、古くから利用されている静的メソッドがいくつか用意されています。

これらはシンプルかつ高速に動作するため、LINQを必要としない単純な検索に適しています。

Array.IndexOf と Array.LastIndexOf

配列の中に特定の要素が含まれているかどうか、そしてその「位置(インデックス)」を知りたい場合に最も効率的なのがArray.IndexOfメソッドです。

このメソッドは、配列の先頭から指定された値を検索し、最初に見つかった位置のインデックスを返します。

値が見つからない場合は-1を返却するため、条件分岐の判定にも利用されます。

C#
using System;

class Program
{
    static void Main()
    {
        string[] fruits = { "apple", "banana", "cherry", "apple", "elderberry" };

        // "apple"を検索(最初に見つかったインデックスを返す)
        int firstIndex = Array.IndexOf(fruits, "apple");
        // 最後から検索
        int lastIndex = Array.LastIndexOf(fruits, "apple");
        // 存在しない要素を検索
        int notFoundIndex = Array.IndexOf(fruits, "orange");

        Console.WriteLine($"最初のappleのインデックス: {firstIndex}");
        Console.WriteLine($"最後のappleのインデックス: {lastIndex}");
        Console.WriteLine($"存在しない場合の戻り値: {notFoundIndex}");
    }
}
実行結果
最初のappleのインデックス: 0
最後のappleのインデックス: 3
存在しない場合の戻り値: -1

Array.Exists による存在確認

値のインデックスは不要で、単に「条件を満たす要素が1つでもあるか」を知りたい場合は、Array.Existsメソッドが便利です。

このメソッドはラムダ式(述語)を引数に取ることができ、柔軟な条件指定が可能です。

C#
using System;

class Program
{
    static void Main()
    {
        int[] numbers = { 10, 20, 30, 40, 50 };

        // 35より大きい数値が存在するか確認
        bool isLargeNumberExists = Array.Exists(numbers, n => n > 35);

        Console.WriteLine($"35より大きい数値はあるか: {isLargeNumberExists}");
    }
}
実行結果
35より大きい数値はあるか: True

Array.Find系メソッドによる柔軟な検索

Array.FindArray.FindAllArray.FindIndexなどのメソッド群は、特定の値を直接指定するのではなく、ラムダ式を用いて検索条件を定義できる点が大きな特徴です。

1つの要素を取得する Array.Find

Array.Findは、指定した条件に一致する「最初の要素」を返します。

条件に一致する要素がない場合、型のデフォルト値(参照型ならnull、数値型なら0)が返される点に注意が必要です。

C#
using System;

class Program
{
    static void Main()
    {
        int[] scores = { 45, 78, 92, 60, 85 };

        // 80点以上の最初のスコアを取得
        int topScore = Array.Find(scores, s => s >= 80);

        Console.WriteLine($"最初の高スコア: {topScore}");
    }
}
実行結果
最初の高スコア: 92

複数の要素を抽出する Array.FindAll

条件に合致するすべての要素を新しい配列として取得したい場合は、Array.FindAllを使用します。

これはフィルタリング操作に相当します。

C#
using System;

class Program
{
    static void Main()
    {
        int[] scores = { 45, 78, 92, 60, 85 };

        // 70点以上の要素をすべて抽出
        int[] highScores = Array.FindAll(scores, s => s >= 70);

        Console.WriteLine("70点以上のスコア一覧:");
        foreach (var score in highScores)
        {
            Console.WriteLine(score);
        }
    }
}
実行結果
70点以上のスコア一覧:
78
92
85

LINQを使用した高度な検索手法

現代のC#開発において、配列検索の主流となっているのがLINQ(Language Integrated Query)です。

LINQは、配列だけでなくリストやデータベースなど、あらゆるデータ集合に対して共通の構文で検索を行える強力なライブラリです。

First と FirstOrDefault の使い分け

LINQで最もよく使われる検索メソッドがFirst()およびFirstOrDefault()です。

  • First(): 条件に一致する要素が見つからない場合、InvalidOperationException例外をスローします。
  • FirstOrDefault(): 見つからない場合に型のデフォルト値(nullなど)を返します。

安全性を考慮し、業務アプリケーションではFirstOrDefault()を使用して、戻り値のnullチェックを行う手法が推奨されます。

C#
using System;
using System.Linq;

class Program
{
    static void Main()
    {
        string[] users = { "Alice", "Bob", "Charlie" };

        // "B"から始まる最初の名前を取得
        string result = users.FirstOrDefault(u => u.StartsWith("B"));

        if (result != null)
        {
            Console.WriteLine($"見つかったユーザー: {result}");
        }
        else
        {
            Console.WriteLine("条件に一致するユーザーはいません。");
        }
    }
}
実行結果
見つかったユーザー: Bob

Where によるフィルタリング

Array.FindAllと同様の機能ですが、LINQのWhereメソッドは「遅延実行」という特徴を持っています。

これは、実際にデータが必要になる(foreachで回す、ToList()を呼ぶなど)まで検索処理が行われない仕組みです。

C#
using System;
using System.Linq;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

        // 偶数のみを抽出
        IEnumerable<int> evenNumbers = numbers.Where(n => n % 2 == 0);

        Console.WriteLine("偶数リスト:");
        foreach (var n in evenNumbers)
        {
            Console.Write(n + " ");
        }
    }
}
実行結果
偶数リスト:
2 4 6 8 10

Any と All

これらは検索というよりも「状態確認」に近いメソッドですが、実務では頻出します。

  • Any(): いずれかの要素が条件を満たしているか(Array.ExistsのLINQ版)。
  • All(): すべての要素が条件を満たしているか。
C#
using System;
using System.Linq;

class Program
{
    static void Main()
    {
        int[] values = { 5, 12, 8, 20 };

        bool hasLargeValue = values.Any(v => v > 15);
        bool areAllPositive = values.All(v => v > 0);

        Console.WriteLine($"15より大きい値はあるか: {hasLargeValue}");
        Console.WriteLine($"すべて正の数か: {areAllPositive}");
    }
}
実行結果
15より大きい値はあるか: True
すべて正の数か: True

パフォーマンスを意識した検索

検索対象となる配列のサイズが数万、数百万を超える場合、検索手法の選択がアプリケーションのパフォーマンスに直結します。

ソート済み配列での二分探索(BinarySearch)

配列が昇順にソートされていることが保証されている場合、Array.BinarySearchを使用することで劇的に検索速度を向上させることができます。

通常のIndexOfが先頭から順に探す(O(n))のに対し、二分探索は範囲を半分に絞り込みながら探す(O(log n))ため、データ量が多いほど有利です。

C#
using System;

class Program
{
    static void Main()
    {
        int[] sortedNumbers = { 10, 25, 30, 45, 50, 60, 75, 80, 95 };

        // 60を高速に検索
        int index = Array.BinarySearch(sortedNumbers, 60);

        if (index >= 0)
        {
            Console.WriteLine($"60はインデックス {index} にあります。");
        }
    }
}
実行結果
60はインデックス 5 にあります。

Span<T> を活用した最新の高速検索

C# 7.2以降で導入され、現在の.NET開発でパフォーマンスチューニングの鍵となっているのがSpan<T>です。

配列の一部を切り出したり、メモリコピーを発生させずに高速に走査したりする場合に有用です。

MemoryExtensions.ContainsIndexOfSpanに対して使用することで、通常の配列メソッドよりも低レベルで最適化された検索が可能です。

C#
using System;

class Program
{
    static void Main()
    {
        int[] largeArray = new int[1000];
        for (int i = 0; i < 1000; i++) largeArray[i] = i;

        // 配列をSpanとして扱う(コピーは発生しない)
        ReadOnlySpan<int> span = largeArray.AsSpan();

        // 500から600の範囲だけをスライスして検索
        ReadOnlySpan<int> slice = span.Slice(500, 100);
        int indexInSlice = slice.IndexOf(550);

        Console.WriteLine($"スライス内でのインデックス: {indexInSlice}");
        Console.WriteLine($"元の配列でのインデックス: {500 + indexInSlice}");
    }
}
実行結果
スライス内でのインデックス: 50
元の配列でのインデックス: 550

検索手法の比較表

状況に応じて最適なメソッドを選択できるよう、主な検索手法を比較表にまとめました。

メソッド名戻り値特徴推奨シーン
Array.IndexOfint単純な値の完全一致検索。高速。値の位置を知りたい時。
Array.FindTラムダ式を使用。最初に見つかった要素を返す。単純な条件で1つ取得したい時。
Array.FindAllT[]条件に合う全ての要素を配列で返す。抽出後のデータも配列として扱いたい時。
LINQ FirstOrDefaultTラムダ式を使用。見つからない場合はnull。柔軟な条件検索の標準。
LINQ WhereIEnumerable<T>遅延実行によるフィルタリング。抽出後にさらに加工(ソート等)を続ける時。
Array.BinarySearchintソート済み配列に対して超高速検索。巨大なソート済みデータの検索。

実践的な使い分けのポイント

配列の検索手法を選ぶ際の判断基準として、以下の3つの観点を意識すると良いでしょう。

1. 可読性と保守性

プロジェクト全体でLINQが多用されている場合、FirstOrDefaultAnyを使うことでコードの意図が明確になります。

LINQは「何をしたいか」を直感的に記述できるため、基本的にはLINQを第一選択肢に据えるのが現代的なC#開発のスタイルです。

2. パフォーマンスの要求レベル

非常に高い頻度(毎秒数万回など)で実行されるループ内での検索や、極めて巨大な配列を扱う場合は、LINQのオーバーヘッド(列挙子の作成など)が無視できなくなることがあります。

そのようなケースでは、Array.IndexOfや、場合によっては伝統的なforループによる手動検索、あるいはSpan<T>の利用を検討してください。

3. 戻り値の型

結果をそのまま配列(T[])として保持し続けたいのか、それとも列挙可能なインターフェース(IEnumerable<T>)として後続の処理に流したいのかによって、Array.FindAllWhereを使い分けます。

多次元配列・ジャグ配列の検索

ここまでは1次元配列を前提としてきましたが、C#の多次元配列やジャグ配列(配列の配列)を検索する場合は少し注意が必要です。

これらに対してLINQを直接適用することはできないため、Cast<T>()を使用するか、入れ子になったループで処理するのが一般的です。

C#
using System;
using System.Linq;

class Program
{
    static void Main()
    {
        // 2次元配列
        int[,] matrix = {
            { 1, 2, 3 },
            { 4, 5, 6 },
            { 7, 8, 9 }
        };

        // LINQを使うために平坦化(Castを使用)
        bool hasSeven = matrix.Cast<int>().Any(n => n == 7);

        // ジャグ配列
        int[][] jagged = new int[][] {
            new int[] { 1, 2 },
            new int[] { 10, 20, 30 }
        };

        // SelectManyで平坦化して検索
        var allValues = jagged.SelectMany(inner => inner);
        int maxVal = allValues.Max();

        Console.WriteLine($"2次元配列に7はあるか: {hasSeven}");
        Console.WriteLine($"ジャグ配列の最大値: {maxVal}");
    }
}
実行結果
2次元配列に7はあるか: True
ジャグ配列の最大値: 30

多次元配列(int[,])はメモリ上に連続して配置されていますが、LINQの標準的な拡張メソッドはIEnumerable<T>を対象とするため、上記のようにキャストが必要になります。

一方、ジャグ配列はSelectManyを用いることでスムーズにLINQチェーンに組み込むことができます。

配列検索における注意点とTips

検索処理を実装する際に陥りやすい罠や、知っておくと便利なテクニックを紹介します。

Null許容型とデフォルト値

Array.FindFirstOrDefaultを使用する際、対象の配列が値型(intなど)である場合、見つからなかったときの結果が「0」になります。

これが「値としての0」なのか「見つからなかった結果の0」なのか判別できない場合があります。

これを避けるには、以下のようにAnyで存在確認をしてから取得するか、配列をint?(Null許容型)にキャストして扱う工夫が必要です。

構造体の検索

配列の要素が構造体(struct)の場合、IndexOfなどのメソッドは内部でEqualsメソッドを呼び出します。

独自の構造体を使用している場合は、IEquatable<T>インターフェースを実装しておくことで、検索時の比較処理を最適化・正当化できます。

大文字・小文字を区別しない文字列検索

文字列の配列から特定の文字を検索する場合、デフォルトでは大文字と小文字が区別されます。

これを無視して検索したい場合は、ラムダ式内でEqualsメソッドとStringComparison列挙型を指定するのがベストプラクティスです。

C#
string[] tags = { "C#", "DotNet", "Azure" };
bool exists = tags.Any(t => t.Equals("c#", StringComparison.OrdinalIgnoreCase));

このように記述することで、ロケールに依存しない高速かつ意図通りの検索が可能になります。

まとめ

C#における配列検索は、非常に多くの選択肢が提供されています。

最後に、本記事で解説した内容のポイントを振り返ります。

  • 単純な値の検索には、高速なArray.IndexOfが最適です。
  • 柔軟な条件検索を行いたい場合は、Array.FindLINQのメソッドを活用しましょう。
  • モダンなC#開発では、可読性と記述性に優れたLINQ(FirstOrDefault, Where, Anyなど)の使用が推奨されます。
  • 究極のパフォーマンスが求められる場面では、Array.BinarySearchSpan<T>による最適化を検討してください。
  • 多次元配列を扱う際は、CastSelectManyによる平坦化、あるいは入れ子のループが必要になります。

検索はデータ操作の基本であり、適切なメソッドを選択することはバグの少ない、メンテナンス性の高いコードへの第一歩です。

扱うデータの規模や、後続の処理でどのようにデータを使いたいかを考慮し、最適な手法を使い分けていきましょう。