C#を用いたソフトウェア開発において、特定の文字列の中にターゲットとなる文字列が含まれているかを判定する「部分一致」の処理は、最も頻繁に利用されるロジックの一つです。

ユーザー入力のバリデーション、ログファイルの解析、データベースから取得したデータのフィルタリングなど、その用途は多岐にわたります。

しかし、一言に「部分一致」と言っても、C#には String.Contains メソッドだけでなく、正規表現を用いた Regex クラスや、コレクションに対して操作を行う LINQ、さらにはパフォーマンスを極限まで追求するための Span<char> を活用した手法など、複数のアプローチが存在します。

これらの手法は、検索の柔軟性や実行速度、メモリ効率の面でそれぞれ異なる特性を持っています。

本記事では、C#における文字列の部分一致判定について、基本的な実装方法から最新の .NET 環境における最適化手法、さらには各手法の性能比較までを詳しく解説します。

開発シーンに合わせた最適な選択ができるよう、具体的なコード例とともに見ていきましょう。

String.Containsによる基本的な部分一致判定

C#で最もシンプルかつ直感的に部分一致を判定する方法が、string.Contains メソッドを使用することです。

このメソッドは、指定した文字列が対象の文字列内に存在するかどうかを bool 型で返します。

基本的な使い方

.NET Core 以降(.NET 5/6/7/8/9を含む)では、Contains メソッドに StringComparison 列挙型を引数として渡すことができるようになり、大文字と小文字を区別しない検索も容易に実装できるようになりました。

C#
using System;

class Program
{
    static void Main()
    {
        string text = "C# Programming with .NET";
        string keyword = "programming";

        // 1. 基本的な使用法(大文字小文字を区別する)
        bool result1 = text.Contains("C#");
        
        // 2. StringComparisonを指定して大文字小文字を無視する
        // .NET Core / .NET 5以降で推奨される書き方
        bool result2 = text.Contains(keyword, StringComparison.OrdinalIgnoreCase);

        Console.WriteLine($"基本検索結果: {result1}");
        Console.WriteLine($"大文字小文字を無視した結果: {result2}");
    }
}
実行結果
基本検索結果: True
大文字小文字を無視した結果: True

StringComparisonの選択基準

StringComparison にはいくつかのオプションがありますが、パフォーマンスと正確性の観点から以下の使い分けが重要です。

Ordinal / OrdinalIgnoreCase

文字のバイナリ値を直接比較します。

最も高速であり、プログラム内部での識別子比較などに適しています。

CurrentCulture / CurrentCultureIgnoreCase

実行環境の言語設定(ロケール)に基づいて比較します。

ユーザーに表示するテキストの検索に適しています。

InvariantCulture / InvariantCultureIgnoreCase

言語に依存しない一定のルールで比較します。

設定ファイルなどの解析に適しています。

通常、特別な理由がない限りは 性能面で有利な Ordinal または OrdinalIgnoreCase を選択する のがベストプラクティスです。

IndexOfメソッドによる詳細な位置特定

単に「含まれているか」だけでなく、「どこに含まれているか」を知りたい場合には IndexOf メソッドを使用します。

IndexOfの返り値と活用

IndexOf は、見つかった文字列の開始インデックスを整数で返します。

見つからなかった場合は -1 を返します。

C#
using System;

class Program
{
    static void Main()
    {
        string source = "The quick brown fox jumps over the lazy dog";
        string target = "fox";

        int index = source.IndexOf(target, StringComparison.OrdinalIgnoreCase);

        if (index != -1)
        {
            Console.WriteLine($"'{target}' はインデックス {index} で見つかりました。");
        }
        else
        {
            Console.WriteLine("見つかりませんでした。");
        }
    }
}
実行結果
'fox' はインデックス 16 で見つかりました。

IndexOf を使用する場合も、Contains と同様に StringComparison を指定することを推奨します。

古い .NET Framework 環境のコードでは String.ToUpper() などを通してから比較する手法が見られますが、これは 一時的な文字列インスタンスを生成するため、メモリ効率が悪化する というデメリットがあります。

Regex(正規表現)による高度なパターンマッチング

「特定の単語で始まり、数字で終わる文字列」といった複雑な条件で部分一致を判定したい場合は、System.Text.RegularExpressions.Regex クラスを使用します。

Regex.IsMatchの使用例

正規表現は非常に強力ですが、単純な文字列比較に比べると処理コスト(CPU負荷)が高くなります。

C#
using System;
using System.Text.RegularExpressions;

class Program
{
    static void Main()
    {
        string input = "Order_ID: 12345 - Status: Completed";
        
        // 「Order_ID: 」の後に数字が続くパターンを検索
        string pattern = @"Order_ID:\s*\d+";

        if (Regex.IsMatch(input, pattern))
        {
            Console.WriteLine("有効なオーダーIDの形式が含まれています。");
        }
    }
}
実行結果
有効なオーダーIDの形式が含まれています。

パフォーマンスを改善するGeneratedRegex

.NET 7 以降では、ソースジェネレーターを用いた [GeneratedRegex] 属性が導入されました。

これにより、実行時ではなくコンパイル時に正規表現の解析が行われるため、劇的なパフォーマンス向上が期待できます。

C#
using System;
using System.Text.RegularExpressions;

partial class OrderProcessor
{
    // コンパイル時に正規表現エンジンを生成する
    [GeneratedRegex(@"Order_ID:\s*\d+")]
    private static partial Regex OrderIdRegex();

    public void Process(string input)
    {
        if (OrderIdRegex().IsMatch(input))
        {
            Console.WriteLine("パターンに一致しました(Source Generator版)");
        }
    }
}

高頻度で正規表現による部分一致判定を行うアプリケーションでは、この手法が推奨されます。

LINQを用いたコレクション内の部分一致検索

文字列のリストや配列の中から、部分一致する要素を抽出する場合には LINQ(Language Integrated Query)が威力を発揮します。

AnyとWhereの使い分け

  • Any: 条件に一致する要素が「一つでもあるか」を確認する(戻り値:bool)
  • Where: 条件に一致する要素を「すべて抽出する」
C#
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        var fruits = new List<string> { "Apple", "Banana", "Blueberry", "Cherry", "Orange" };
        string searchWord = "blue";

        // 部分一致するものが存在するか判定
        bool hasMatch = fruits.Any(f => f.Contains(searchWord, StringComparison.OrdinalIgnoreCase));

        // 部分一致する要素をすべて取得
        var matches = fruits.Where(f => f.Contains(searchWord, StringComparison.OrdinalIgnoreCase)).ToList();

        Console.WriteLine($"一致する要素の有無: {hasMatch}");
        Console.WriteLine("一致した要素の一覧:");
        matches.ForEach(m => Console.WriteLine($"- {m}"));
    }
}
実行結果
一致する要素の有無: True
一致した要素の一覧:
- Blueberry

LINQは内部的にループ処理を行っているため、データ量が数百万件を超えるような巨大なコレクションに対しては、並列処理を行う AsParallel() (PLINQ)の検討も視野に入ります。

高度な最適化:ReadOnlySpanを用いた部分一致

.NET Core 2.1 以降で導入された Span<T> および ReadOnlySpan<T> は、メモリの割り当て(アロケーション)を抑えつつ、高速な文字列操作を可能にします。

Spanによる効率的なスライスと比較

通常、文字列の一部を切り出して比較しようとすると Substring メソッドを使用しますが、これは新しい文字列オブジェクトをヒープメモリに作成してしまいます。

対して Span は、元のメモリ領域を参照するだけなので、メモリ消費をゼロに抑える ことができます。

C#
using System;

class Program
{
    static void Main()
    {
        string largeText = "ApplicationLog_20240520_Error_DatabaseConnectionFailed";
        
        // ReadOnlySpanとして扱う
        ReadOnlySpan<char> span = largeText.AsSpan();

        // 特定の範囲をスライスして比較(新しい文字列は生成されない)
        bool isError = span.Slice(24, 5).Equals("Error", StringComparison.Ordinal);

        Console.WriteLine($"エラーフラグの判定: {isError}");
    }
}
実行結果
エラーフラグの判定: True

大量のログファイル解析や、リアルタイム通信のパケット解析など、GC(ガベージコレクション)の負荷を最小限に抑えたい極限のパフォーマンスが求められる場面では、この手法が最適です。

各手法の性能比較と使い分けガイド

ここでは、これまで紹介した各手法の特性を比較表にまとめます。

手法柔軟性パフォーマンス推奨される用途
Contains最高単純なキーワードの有無を判定する場合
IndexOf出現位置を特定する必要がある場合
Regex最高低~中パターンマッチングや複雑な抽出条件がある場合
LINQリストや配列内の要素をフィルタリングする場合
Span<char>極めて高いメモリ効率を最優先する大規模データ処理

性能の注意点

Regexのオーバーヘッド

正規表現は初回呼び出し時にコンパイルコストが発生します。

これを緩和するには、static なインスタンスで再利用するか、コンパイル済みコードを生成する GeneratedRegex を使用してください。

StringComparisonの重要性

文字列比較で既定の比較を使うとカルチャ依存のルールによるオーバーヘッドが発生する場合があります。

StringComparison.Ordinal は文化差を無視してバイナリ比較を行うため、一般に最も高速です。

ボクシングとアロケーション

ループ内で Substring を多用すると新しい文字列が大量に生成され、GC 発生頻度が上がりシステム全体のレスポンスが低下します。

代替として ReadOnlySpan<char> などの非アロケーション手法を検討してください。

特殊なケース:複数キーワードのいずれかに一致するか

実務では「複数のキーワードのうち、どれか一つでも含まれているか」を判定したい場面があります。

この場合、LINQと Any を組み合わせるのが最も効率的で読みやすいコードになります。

C#
using System;
using System.Linq;

class Program
{
    static void Main()
    {
        string message = "System alert: Disk space is low on server A";
        string[] keywords = { "Error", "Critical", "Alert", "Warning" };

        // いずれかのキーワードが含まれているか判定
        bool isPriority = keywords.Any(k => message.Contains(k, StringComparison.OrdinalIgnoreCase));

        if (isPriority)
        {
            Console.WriteLine("優先度の高いメッセージを検知しました。");
        }
    }
}
実行結果
優先度の高いメッセージを検知しました。

もしキーワードが数百件、数千件とある場合は、HashSet<string> を活用したり、Aho-Corasickアルゴリズムなどの高度な文字列探索アルゴリズムを検討する必要がありますが、一般的なビジネスロジックであれば上記の LINQ 方式で十分なパフォーマンスが得られます。

大文字小文字の区別に関する落とし穴

C#で部分一致を扱う際、初心者が陥りやすいミスに「全て小文字に変換してから比較する」というものがあります。

C#
// 非推奨な例
if (text.ToLower().Contains(keyword.ToLower())) 
{ 
    // 処理
}

このコードの問題点は、ToLower() を呼び出すたびに新しい文字列インスタンスがヒープ上に作成されることです。

ループの中でこの処理を行うと、メモリ消費量が急増します。

前述した通り、StringComparison.OrdinalIgnoreCase を引数に取るオーバーロードを使用することで、メモリ消費を抑えつつ安全に比較を行うことができます。

また、トルコ語のように「i」の大文字が「İ」になるような特殊な言語規則を考慮する必要がある場合は、StringComparison.CurrentCultureIgnoreCase を使用してください。

一般的な英語や日本語のシステム開発であれば、OrdinalIgnoreCase が最も安全で高速な選択肢となります。

まとめ

C#における文字列の部分一致判定は、単純な Contains から、パフォーマンスに特化した Span、柔軟性の高い Regex まで、用途に応じて最適な手段を選択することが重要です。

本記事のポイントをまとめます。

  • 標準的な検索には、String.Contains または String.IndexOf を使用し、必ず StringComparison を明示する。
  • 複雑な条件には正規表現(Regex)を使用し、.NET 7以降であれば GeneratedRegex による高速化を図る。
  • コレクションの操作には LINQ の AnyWhere を活用し、可読性の高いコードを記述する。
  • 極限のパフォーマンスが必要な場合は、ReadOnlySpan<char> を用いてメモリ割り当てを排除する。
  • 文字列の変換(ToLower/ToUpper)による比較は避け、比較オプションを活用してメモリ効率を高める。

これらの手法を適切に使い分けることで、堅牢かつ高速なC#アプリケーションを構築できるようになります。

プログラムのボトルネックになりやすい文字列操作だからこそ、最新の言語仕様を理解し、最適な実装を心がけましょう。