文字列

【C#】文字列 比較 前方一致をStartsWithで実装する方法と高速化ポイント

C#で前方一致を調べるならStartsWithを使うのが簡単で高速です。

StringComparison.OrdinalIgnoreCaseを指定すれば大文字小文字を無視した安全な比較になり、カルチャ依存の誤判定を防げます。

部分一致が不要ならAsSpan()で範囲を絞ると更に効率が上がります。

前方一致とは何か

文字列の比較において「前方一致」とは、ある文字列が別の文字列の先頭部分と一致しているかどうかを判定することを指します。

たとえば、文字列 "HelloWorld" に対して "Hello" が前方一致しているかを調べる場合、"HelloWorld" の先頭5文字が "Hello" と同じであれば前方一致と判断します。

前方一致は、検索やフィルタリング、入力補完などさまざまな場面で利用されます。

たとえば、ユーザーが入力した文字列に対して候補を絞り込む際に、候補の文字列がユーザー入力の先頭部分と一致しているかを判定することが多いです。

完全一致との違い

「完全一致」とは、比較対象の文字列がまったく同じ内容であることを意味します。

つまり、文字列の長さも含めてすべての文字が同じである場合に完全一致と判定されます。

たとえば、文字列 "HelloWorld""HelloWorld" は完全一致ですが、"HelloWorld""Hello" は完全一致ではありません。

比較対象1比較対象2完全一致の判定理由
HelloWorldHelloWorldtrue文字列が完全に同じ
HelloWorldHellofalse文字列の長さや内容が異なる
HelloHelloWorldfalse文字列の長さや内容が異なる

完全一致は、文字列の内容が完全に同じかどうかを判定したい場合に使います。

前方一致は、文字列の先頭部分だけを比較したい場合に使うため、用途が異なります。

部分一致との違い

「部分一致」とは、比較対象の文字列のどこかに指定した文字列が含まれているかどうかを判定することです。

部分一致は文字列の先頭に限らず、途中や末尾に指定した文字列が存在すれば一致とみなします。

たとえば、文字列 "HelloWorld" に対して "World" が部分一致しているかを調べる場合、"HelloWorld" の中に "World" が含まれているため部分一致と判定されます。

比較対象1比較対象2部分一致の判定理由
HelloWorldHellotrue先頭に含まれている
HelloWorldWorldtrue途中に含まれている
HelloWorldorldtrue途中に含まれている
HelloWorldhifalse含まれていない

部分一致は、文字列のどの位置に指定文字列があるかを問わず、含まれていれば一致と判定します。

これに対して前方一致は、文字列の先頭部分だけを比較するため、より限定的な条件となります。

まとめると、前方一致は「文字列の先頭が指定文字列と同じかどうか」を判定し、完全一致は「文字列全体が同じかどうか」を判定し、部分一致は「文字列のどこかに指定文字列が含まれているか」を判定します。

これらの違いを理解することで、用途に応じた適切な文字列比較方法を選択できます。

StartsWithメソッド徹底解剖

シグネチャ一覧

StartsWith(string value)

このオーバーロードは、指定した文字列が対象の文字列の先頭に一致するかどうかを判定します。

大文字・小文字の区別は行われ、カルチャは現在のカルチャに基づいて比較されます。

string str = "HelloWorld";
bool result = str.StartsWith("Hello"); // true

このメソッドは、引数に渡した文字列がnullの場合、ArgumentNullExceptionをスローします。

また、空文字列を渡すと常にtrueを返します。

StartsWith(string value, StringComparison comparisonType)

このオーバーロードは、比較方法を指定できる点が特徴です。

StringComparison列挙体を使って、大文字・小文字の区別やカルチャの影響を制御できます。

string str = "HelloWorld";
// 大文字小文字を区別しない比較
bool result = str.StartsWith("hello", StringComparison.OrdinalIgnoreCase); // true

StringComparisonの主な値は以下の通りです。

説明
Ordinalバイナリ比較(大文字小文字を区別)
OrdinalIgnoreCaseバイナリ比較(大文字小文字を区別しない)
CurrentCulture現在のカルチャに基づく比較(区別あり)
CurrentCultureIgnoreCase現在のカルチャに基づく比較(区別なし)
InvariantCulture不変カルチャに基づく比較(区別あり)
InvariantCultureIgnoreCase不変カルチャに基づく比較(区別なし)

このオーバーロードを使うことで、意図した比較方法を明示的に指定でき、誤った比較結果を防げます。

StartsWith(char value)

このオーバーロードは、文字列の先頭が指定した単一の文字と一致するかどうかを判定します。

文字列が空の場合は常にfalseを返します。

string str = "HelloWorld";
bool result = str.StartsWith('H'); // true

単一文字の比較を行う場合に便利で、文字列の先頭文字だけをチェックしたいときに使います。

返り値の意味

StartsWithメソッドは、対象の文字列が指定した文字列または文字で始まっている場合にtrueを返し、そうでなければfalseを返します。

判定は引数の内容と比較方法に依存します。

  • 指定した文字列が空文字列の場合は常にtrueを返します。これは空文字列はどの文字列の先頭にも存在するとみなされるためです
  • 対象の文字列がnullの場合はNullReferenceExceptionが発生しますので、呼び出し前にnullチェックが必要です
  • 引数の文字列がnullの場合はArgumentNullExceptionがスローされます

代表的な使用例

以下は、StartsWithメソッドの代表的な使い方を示したサンプルコードです。

using System;
class Program
{
    static void Main()
    {
        string input = "CSharpProgramming";
        // 大文字小文字を区別して前方一致を判定
        bool isMatch1 = input.StartsWith("CSharp");
        Console.WriteLine($"大文字小文字区別あり: {isMatch1}"); // true
        // 大文字小文字を区別しない比較
        bool isMatch2 = input.StartsWith("csharp", StringComparison.OrdinalIgnoreCase);
        Console.WriteLine($"大文字小文字区別なし: {isMatch2}"); // true
        // 先頭文字だけを比較
        bool isMatch3 = input.StartsWith('C');
        Console.WriteLine($"先頭文字が 'C' か: {isMatch3}"); // true
        // 空文字列を指定した場合
        bool isMatch4 = input.StartsWith("");
        Console.WriteLine($"空文字列指定: {isMatch4}"); // true
        // nullを指定すると例外になるためコメントアウト
        // bool isMatch5 = input.StartsWith(null);
    }
}
大文字小文字区別あり: True
大文字小文字区別なし: True
先頭文字が 'C' か: True
空文字列指定: True

この例では、StartsWithの基本的な使い方を示しています。

大文字小文字を区別する場合は引数を1つだけ渡し、区別しない場合はStringComparison.OrdinalIgnoreCaseを指定します。

また、単一文字の比較も簡単に行えます。

空文字列を指定すると常にtrueになる点は覚えておくと便利です。

逆にnullを渡すと例外が発生するため、引数がnullでないことを事前に確認することが重要です。

StringComparisonオプションの詳細

Ordinal と OrdinalIgnoreCase

StringComparison.Ordinalは、文字列をバイナリコードポイントの順序で比較します。

つまり、文字のUnicodeコード値をそのまま比較するため、大文字と小文字は区別されます。

高速であり、カルチャに依存しないため、システム内部の識別子やファイルパスの比較に適しています。

一方、StringComparison.OrdinalIgnoreCaseは、バイナリ比較を行いながら大文字小文字を区別しません。

Unicodeの大文字・小文字の規則に基づいて比較されるため、例えば"Hello""hello"は一致と判定されます。

こちらも高速で、カルチャに依存しないため、ケースを無視した識別子比較に向いています。

string str = "Example";
// Ordinal(大文字小文字区別あり)
bool result1 = str.StartsWith("ex", StringComparison.Ordinal); // false
// OrdinalIgnoreCase(大文字小文字区別なし)
bool result2 = str.StartsWith("ex", StringComparison.OrdinalIgnoreCase); // true

CurrentCulture と CurrentCultureIgnoreCase

StringComparison.CurrentCultureは、現在のスレッドのカルチャ設定に基づいて文字列を比較します。

カルチャ固有のルールを考慮するため、言語や地域に依存した比較が必要な場合に使います。

大文字小文字は区別されます。

StringComparison.CurrentCultureIgnoreCaseは、同じく現在のカルチャに基づきますが、大文字小文字を区別しません。

たとえば、トルコ語のIıのような特殊なケースもカルチャに応じて正しく比較されます。

string str = "straße"; // ドイツ語の「通り」
// CurrentCulture(大文字小文字区別あり)
bool result1 = str.StartsWith("Str", StringComparison.CurrentCulture); // false
// CurrentCultureIgnoreCase(大文字小文字区別なし)
bool result2 = str.StartsWith("Str", StringComparison.CurrentCultureIgnoreCase); // true

ただし、カルチャ依存の比較はパフォーマンスがやや低下することがあり、また環境によって結果が異なる可能性があるため注意が必要です。

InvariantCulture と InvariantCultureIgnoreCase

StringComparison.InvariantCultureは、カルチャに依存しない固定のルールで文字列を比較します。

現在のカルチャに影響されず、一貫した比較結果が求められる場合に使います。

大文字小文字は区別されます。

StringComparison.InvariantCultureIgnoreCaseは、同じく不変カルチャに基づきますが、大文字小文字を区別しません。

たとえば、ログファイルの解析や設定ファイルのキー比較など、環境に依存しない比較が必要な場面で有効です。

string str = "café";
// InvariantCulture(大文字小文字区別あり)
bool result1 = str.StartsWith("CAF", StringComparison.InvariantCulture); // false
// InvariantCultureIgnoreCase(大文字小文字区別なし)
bool result2 = str.StartsWith("CAF", StringComparison.InvariantCultureIgnoreCase); // true

推奨設定と選定基準

文字列の前方一致を判定する際のStringComparisonの選択は、用途やパフォーマンス要件に応じて決めることが重要です。

用途例推奨設定理由
システム内部の識別子やファイル名Ordinal または OrdinalIgnoreCase高速でカルチャに依存しないため一貫性がある
ユーザー向けの表示文字列比較CurrentCulture または CurrentCultureIgnoreCaseユーザーの言語・地域に合わせた自然な比較が可能
環境に依存しない一貫した比較InvariantCulture または InvariantCultureIgnoreCase環境差を排除し、安定した比較結果を得られる

パフォーマンスを重視する場合はOrdinal系を選び、ユーザーの言語環境に配慮したい場合はCurrentCulture系を使うのが一般的です。

InvariantCultureは、環境に依存しない比較が必要なログ解析や設定ファイル処理などで役立ちます。

また、大文字小文字の区別が不要な場合はIgnoreCase付きのオプションを使うことで、より柔軟な比較が可能です。

比較方法を明示的に指定することで、意図しない比較結果を防ぎ、バグの発生を抑えられます。

カルチャによる落とし穴

トルコ語の I 問題

トルコ語には、英語など多くの言語とは異なる大文字・小文字の変換ルールがあります。

特に問題となるのが、ラテン文字の Ii の扱いです。

英語圏では大文字の I は小文字の i に対応しますが、トルコ語では大文字の I に対応する小文字は点のない ı(ドットレス・アイ)であり、小文字の i に対応する大文字は点のある İ(ドット付きアイ)です。

この違いは文字列比較や大文字小文字変換で予期せぬ結果を招くことがあります。

たとえば、StartsWithCurrentCultureIgnoreCaseでトルコ語カルチャに設定して比較すると、"Istanbul""istanbul"が一致しない場合があります。

using System;
using System.Globalization;
using System.Threading;
class Program
{
    static void Main()
    {
        string str = "Istanbul";
        // トルコ語カルチャに設定
        Thread.CurrentThread.CurrentCulture = new CultureInfo("tr-TR");
        // 大文字小文字を区別しない比較
        bool result = str.StartsWith("istanbul", StringComparison.CurrentCultureIgnoreCase);
        Console.WriteLine(result); // false
    }
}
False

このように、トルコ語のカルチャではIiの対応が英語と異なるため、カルチャ依存の比較を行う際は注意が必要です。

誤った比較を避けるためには、カルチャを固定するか、OrdinalIgnoreCaseを使うことが推奨されます。

区分け文字と結合文字

Unicodeでは、文字が複数のコードポイントで表現される場合があります。

たとえば、アクセント付きの文字は「基本文字」と「結合アクセント」の組み合わせで表されることがあります。

これを「区分け文字(combining character)」や「結合文字(combining mark)」と呼びます。

たとえば、é は単一のコードポイント(U+00E9)としても表せますが、e(U+0065)と結合アクセント(U+0301)の組み合わせでも表現できます。

この違いは文字列の比較に影響を与えます。

string composed = "é"; // 単一コードポイント
string decomposed = "e\u0301"; // e + 結合アクセント
bool result1 = composed == decomposed; // false
bool result2 = composed.StartsWith(decomposed); // false
False
False

カルチャ依存の比較では、これらの違いを考慮して正規化(Normalization)を行うことが重要です。

正規化を行うことで、異なる表現の文字列を同一視できます。

using System.Text;
string normalizedComposed = composed.Normalize(NormalizationForm.FormC);
string normalizedDecomposed = decomposed.Normalize(NormalizationForm.FormC);
bool normalizedResult = normalizedComposed == normalizedDecomposed; // true

このように、Unicodeの区分け文字や結合文字の扱いを理解し、必要に応じて正規化を行うことで、文字列比較の誤判定を防げます。

カルチャを固定するメリット

カルチャ依存の文字列比較は、ユーザーの言語や地域設定に応じた自然な比較を実現しますが、環境によって結果が変わるリスクがあります。

特にサーバー環境や多言語対応のアプリケーションでは、予期せぬ動作を招くことがあります。

カルチャを固定することで、比較結果の一貫性と予測可能性を確保できます。

たとえば、InvariantCultureOrdinalを使うことで、どの環境でも同じ比較結果が得られます。

string str = "straße";
bool resultInvariant = str.StartsWith("Str", StringComparison.InvariantCultureIgnoreCase);
bool resultOrdinal = str.StartsWith("Str", StringComparison.OrdinalIgnoreCase);
Console.WriteLine(resultInvariant); // true
Console.WriteLine(resultOrdinal);    // false
True
False

この例では、InvariantCultureIgnoreCaseはドイツ語のßssと同等とみなすためtrueを返しますが、OrdinalIgnoreCaseは単純なバイナリ比較のためfalseとなります。

用途に応じて適切なカルチャを選択し、必要なら固定することが重要です。

まとめると、カルチャを固定することで以下のメリットがあります。

  • 比較結果の一貫性が保てる
  • 環境依存のバグを防げる
  • パフォーマンスが向上する場合がある

特にシステム内部の識別子やファイル名の比較にはOrdinal系を使い、ユーザー向けの表示文字列にはCurrentCulture系を使うなど、使い分けが推奨されます。

パフォーマンス最適化

AsSpan()の導入効果

AsSpan()は、文字列からReadOnlySpan<char>を取得するメソッドで、文字列の部分的な操作や比較を効率的に行うために使います。

ReadOnlySpan<char>はスタック上に存在し、ヒープアロケーションを発生させないため、メモリ効率が高く高速です。

前方一致の判定において、StartsWithの代わりにAsSpan()を使って部分文字列を取得し、SequenceEqualで比較する方法があります。

これにより、文字列の切り出しや新たな文字列生成を避けられ、パフォーマンスが向上します。

using System;
class Program
{
    static void Main()
    {
        string source = "PerformanceOptimization";
        string prefix = "Performance";
        // AsSpanを使った前方一致判定
        bool isMatch = source.AsSpan(0, prefix.Length).SequenceEqual(prefix.AsSpan());
        Console.WriteLine(isMatch); // true
    }
}
True

この方法は、特に大量の文字列を高速に処理したい場合に効果的です。

ReadOnlySpan<char>でアロケーション削減

通常、文字列の部分比較や切り出しを行うと、新しい文字列オブジェクトが生成されヒープアロケーションが発生します。

これが大量に繰り返されるとGC(ガベージコレクション)の負荷が増大し、パフォーマンス低下の原因となります。

ReadOnlySpan<char>は文字列のメモリを直接参照するため、部分文字列の操作でも新たな文字列を生成しません。

これにより、アロケーションを大幅に削減でき、GCの負荷を軽減します。

string text = "OptimizationExample";
ReadOnlySpan<char> span = text.AsSpan(0, 11); // "Optimization"
Console.WriteLine(span.ToString()); // "Optimization"

ReadOnlySpan<char>はスタック上に存在し、軽量で高速なため、パフォーマンスクリティカルな処理に適しています。

ループとLINQの速度差

文字列の前方一致判定を複数回行う場合、forループを使った手動の比較とLINQを使った比較ではパフォーマンスに差が出ます。

LINQはコードが簡潔になる反面、内部でデリゲートやイテレータを使うため、オーバーヘッドが発生しやすいです。

特に大量の文字列を処理する場合は、ループを使った明示的な比較の方が高速です。

using System;
using System.Linq;
class Program
{
    static void Main()
    {
        string[] words = { "apple", "application", "banana", "apply" };
        string prefix = "app";
        // LINQを使った前方一致判定
        var linqMatches = words.Where(w => w.StartsWith(prefix)).ToArray();
        // ループを使った前方一致判定
        var loopMatches = new System.Collections.Generic.List<string>();
        foreach (var word in words)
        {
            if (word.Length >= prefix.Length)
            {
                bool isMatch = true;
                for (int i = 0; i < prefix.Length; i++)
                {
                    if (word[i] != prefix[i])
                    {
                        isMatch = false;
                        break;
                    }
                }
                if (isMatch) loopMatches.Add(word);
            }
        }
        Console.WriteLine("LINQ Matches: " + string.Join(", ", linqMatches));
        Console.WriteLine("Loop Matches: " + string.Join(", ", loopMatches));
    }
}
LINQ Matches: apple, application, apply
Loop Matches: apple, application, apply

ループの方が若干高速でメモリ効率も良いため、パフォーマンスが重要な場面ではループを検討してください。

事前正規化で比較コスト削減

Unicode文字列は複数の表現方法が存在し、比較時に正規化が必要になることがあります。

正規化を毎回比較時に行うとコストが高くなります。

事前に文字列を正規化しておくことで、比較時の負荷を減らせます。

特に大量の文字列を繰り返し比較する場合は、正規化済みの文字列を保持し、比較時は単純なバイナリ比較やStartsWithを使うと効率的です。

using System;
using System.Text;
class Program
{
    static void Main()
    {
        string original = "e\u0301"; // e + 結合アクセント
        string normalized = original.Normalize(NormalizationForm.FormC);
        string prefix = "é"; // 単一コードポイント
        string normalizedPrefix = prefix.Normalize(NormalizationForm.FormC);
        bool isMatch = normalized.StartsWith(normalizedPrefix, StringComparison.Ordinal);
        Console.WriteLine(isMatch); // true
    }
}
True

このように、事前正規化により比較コストを削減し、正確かつ高速な前方一致判定が可能になります。

バッファリング戦略

大量の文字列を連続して比較する場合、バッファリングを活用するとパフォーマンスが向上します。

たとえば、入力データを一括で読み込み、メモリ上のバッファに保持してから処理する方法です。

バッファリングにより、I/O回数を減らし、CPUキャッシュの効率的な利用が可能になります。

特にファイルやネットワークからの大量データ処理で効果的です。

using System;
using System.IO;
using System.Text;
class Program
{
    static void Main()
    {
        string filePath = "data.txt";
        string prefix = "Test";
        // ファイル全体を一度に読み込む
        string content = File.ReadAllText(filePath, Encoding.UTF8);
        // 改行で分割して前方一致判定
        string[] lines = content.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
        foreach (var line in lines)
        {
            if (line.StartsWith(prefix, StringComparison.Ordinal))
            {
                Console.WriteLine(line);
            }
        }
    }
}

バッファリング戦略は、I/Oのオーバーヘッドを減らし、CPU処理を効率化するため、パフォーマンス改善に寄与します。

状況に応じて適切に活用してください。

大規模データ処理の応用

ファイルストリームでの行フィルター

大きなテキストファイルを扱う際、全体を一度にメモリに読み込むのは非効率であり、メモリ不足の原因にもなります。

ファイルストリームを使って逐次的に読み込みながら、前方一致で行をフィルターする方法が有効です。

StreamReaderを使い、1行ずつ読み込んでStartsWithで判定し、条件に合う行だけを処理または出力します。

これによりメモリ使用量を抑えつつ、大規模ファイルの検索や抽出が可能です。

using System;
using System.IO;
class Program
{
    static void Main()
    {
        string filePath = "largefile.txt";
        string prefix = "Error";
        using (var reader = new StreamReader(filePath))
        {
            string line;
            while ((line = reader.ReadLine()) != null)
            {
                if (line.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
                {
                    Console.WriteLine(line);
                }
            }
        }
    }
}
Error: ファイルが見つかりません。
Error: ネットワーク接続が切断されました。
...

この方法は、ファイルサイズが非常に大きくても安定して動作し、リアルタイムログ解析や大量データのフィルタリングに適しています。

メモリマップトファイルの高速検索

メモリマップトファイル(Memory-Mapped File)は、ファイルの内容を仮想メモリにマッピングし、ファイル全体をメモリ上の配列のように扱える仕組みです。

これにより、大容量ファイルの高速アクセスが可能になります。

前方一致検索を行う場合、メモリマップトファイルを使ってファイルの一部を効率的に読み込み、ReadOnlySpan<char>などで比較することで高速化が期待できます。

using System;
using System.IO.MemoryMappedFiles;
using System.Text;
class Program
{
    static void Main()
    {
        string filePath = "largefile.txt";
        string prefix = "Warning";
        using (var mmf = MemoryMappedFile.CreateFromFile(filePath, System.IO.FileMode.Open))
        {
            using (var accessor = mmf.CreateViewAccessor(0, 0, MemoryMappedFiles.MemoryMappedFileAccess.Read))
            {
                long length = accessor.Capacity;
                byte[] buffer = new byte[length];
                accessor.ReadArray(0, buffer, 0, (int)length);
                string content = Encoding.UTF8.GetString(buffer);
                string[] lines = content.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
                foreach (var line in lines)
                {
                    if (line.StartsWith(prefix, StringComparison.Ordinal))
                    {
                        Console.WriteLine(line);
                    }
                }
            }
        }
    }
}
Warning: メモリ使用率が高いです。
Warning: ディスク容量が不足しています。
...

メモリマップトファイルは、ファイルの一部だけを効率的にアクセスできるため、特に大規模ファイルのランダムアクセスや高速検索に適しています。

パラレル処理と並列LINQ

大量の文字列データを前方一致でフィルターする際、CPUのマルチコアを活用することで処理時間を短縮できます。

Parallel.ForEachや並列LINQ(PLINQ)を使うと、複数のスレッドで同時に処理を行えます。

以下は、文字列配列に対して並列でStartsWithを使い、前方一致する要素を抽出する例です。

using System;
using System.Linq;
using System.Threading.Tasks;
class Program
{
    static void Main()
    {
        string[] data = Enumerable.Range(1, 1000000).Select(i => "Item" + i).ToArray();
        string prefix = "Item999";
        // PLINQを使った並列フィルター
        var matches = data.AsParallel()
                          .WithDegreeOfParallelism(Environment.ProcessorCount)
                          .Where(s => s.StartsWith(prefix))
                          .ToArray();
        Console.WriteLine($"一致した件数: {matches.Length}");
        if (matches.Length > 0)
        {
            Console.WriteLine($"最初の一致: {matches[0]}");
        }
    }
}
一致した件数: 111
最初の一致: Item999

Parallel.ForEachを使う場合は、スレッドセーフなコレクションやロックを適切に使う必要がありますが、PLINQは内部で並列処理を管理してくれるため簡単に並列化できます。

ただし、並列処理はスレッドの切り替えコストや同期処理のオーバーヘッドがあるため、データ量や処理内容に応じて効果が変わります。

小規模データでは逆に遅くなることもあるため、パフォーマンス測定を行いながら適用してください。

代替手段との比較

IndexOfでの開始位置チェック

IndexOfメソッドは、指定した文字列や文字が対象の文字列内で最初に現れる位置を返します。

前方一致の判定においては、検索対象の文字列が先頭(インデックス0)にあるかどうかを確認することで実現できます。

string str = "HelloWorld";
string prefix = "Hello";
bool isStartsWith = str.IndexOf(prefix, StringComparison.Ordinal) == 0;
Console.WriteLine(isStartsWith); // true
True

IndexOfは部分文字列の位置を返すため、先頭以外に存在しても検出されますが、前方一致判定ではインデックスが0かどうかをチェックします。

StartsWithと比べて柔軟に使えますが、意図しない位置での一致を見逃さないように注意が必要です。

また、IndexOfStringComparisonを指定できるため、大文字小文字の区別やカルチャ依存の比較も可能です。

ただし、StartsWithの方が前方一致専用であるため、コードの可読性は高いです。

Compareによる範囲比較

string.Compareメソッドは、2つの文字列を比較し、等しい場合は0を返します。

前方一致を判定するには、対象文字列の先頭部分と比較対象文字列を比較し、結果が0であれば前方一致と判断できます。

string str = "HelloWorld";
string prefix = "Hello";
bool isStartsWith = string.Compare(str, 0, prefix, 0, prefix.Length, StringComparison.Ordinal) == 0;
Console.WriteLine(isStartsWith); // true
True

この方法は、Compareのオーバーロードで開始位置と比較長を指定できるため、部分的な比較に適しています。

StartsWithと同様にStringComparisonを指定できるため、大文字小文字の区別やカルチャ依存の制御も可能です。

ただし、Compareは整数を返すため、結果の解釈が必要であり、コードの可読性はStartsWithに劣る場合があります。

正規表現の ^ アンカー活用

正規表現を使うと、文字列の先頭に特定のパターンがあるかどうかを柔軟に判定できます。

^は文字列の先頭を示すアンカーで、これを使って前方一致を表現します。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string str = "HelloWorld";
        string pattern = "^Hello";
        bool isMatch = Regex.IsMatch(str, pattern, RegexOptions.IgnoreCase);
        Console.WriteLine(isMatch); // true
    }
}
True

正規表現は複雑なパターンマッチングに強力ですが、単純な前方一致判定にはオーバーヘッドが大きく、パフォーマンスが低下する可能性があります。

特に大量の文字列を高速に処理したい場合は注意が必要です。

また、正規表現のオプションで大文字小文字の区別を制御できるため、柔軟な比較が可能です。

カスタムアルゴリズムの実装

特定の要件やパフォーマンス最適化のために、独自の前方一致判定アルゴリズムを実装することもあります。

たとえば、文字列の先頭から1文字ずつ比較し、異なる文字があれば即座に判定を終了する方法です。

using System;

class Program
{
    static bool CustomStartsWith(string source, string prefix, bool ignoreCase = false)
    {
        if (source == null || prefix == null) return false;
        if (prefix.Length > source.Length) return false;
        for (int i = 0; i < prefix.Length; i++)
        {
            char c1 = source[i];
            char c2 = prefix[i];
            if (ignoreCase)
            {
                c1 = char.ToUpperInvariant(c1);
                c2 = char.ToUpperInvariant(c2);
            }
            if (c1 != c2) return false;
        }
        return true;
    }

    static void Main()
    {
        string str = "HelloWorld";
        string prefix = "hello";
        bool result = CustomStartsWith(str, prefix, ignoreCase: true);
        Console.WriteLine(result); // true
    }
}
True

この方法は、StartsWithの内部処理に似ていますが、必要に応じて大文字小文字の比較方法やカルチャ処理をカスタマイズできます。

ただし、既存のStartsWithは最適化されているため、特別な理由がない限り自作は推奨されません。

カスタム実装は、特殊な比較ルールやパフォーマンスチューニングが必要な場合に検討してください。

例外対策と思わぬ罠

Null 値の安全な扱い

StartsWithメソッドを使う際に最も注意すべきポイントの一つが、null値の扱いです。

対象の文字列がnullの場合、StartsWithを呼び出すとNullReferenceExceptionが発生します。

また、引数に渡す比較文字列がnullの場合はArgumentNullExceptionがスローされます。

string source = null;
string prefix = "test";
// NullReferenceExceptionが発生する例
// bool result = source.StartsWith(prefix); // 実行時エラー
// ArgumentNullExceptionが発生する例
string source2 = "example";
// bool result2 = source2.StartsWith(null); // 実行時エラー

これらの例外を防ぐためには、呼び出し前にnullチェックを行うことが重要です。

安全に扱うための方法としては、以下のようなガード節を使うのが一般的です。

bool SafeStartsWith(string source, string prefix)
{
    if (source == null || prefix == null)
        return false;
    return source.StartsWith(prefix);
}

このように、nullを許容しない場合はfalseを返すことで例外を回避できます。

nullを許容するかどうかはアプリケーションの仕様に応じて判断してください。

空文字列の境界条件

StartsWithメソッドに空文字列("")を渡した場合、常にtrueを返します。

これは空文字列がどの文字列の先頭にも存在するとみなされるためです。

string source = "HelloWorld";
bool result = source.StartsWith("");
Console.WriteLine(result); // true
True

この挙動は仕様として正しいものですが、意図しない結果を招くことがあります。

たとえば、ユーザー入力が空文字列の場合に前方一致判定を行うと、常にtrueとなり、フィルターが正しく機能しない可能性があります。

空文字列を特別扱いしたい場合は、呼び出し前にチェックを入れることが推奨されます。

bool IsValidPrefix(string prefix)
{
    return !string.IsNullOrEmpty(prefix);
}
if (IsValidPrefix(prefix) && source.StartsWith(prefix))
{
    // 処理
}

例外を避けるガード節

例外を未然に防ぐためには、StartsWithを呼び出す前に適切なガード節を設けることが重要です。

特にnullチェックと空文字列チェックを組み合わせることで、安全かつ意図した動作を保証できます。

bool SafeStartsWith(string source, string prefix, StringComparison comparison = StringComparison.Ordinal)
{
    if (string.IsNullOrEmpty(source) || string.IsNullOrEmpty(prefix))
        return false;
    return source.StartsWith(prefix, comparison);
}

この関数は、sourceまたはprefixnullまたは空文字列の場合にfalseを返し、それ以外はStartsWithを呼び出します。

これにより、例外の発生を防ぎつつ、空文字列の特別な挙動も回避できます。

また、呼び出し元で例外処理を行うよりも、こうしたガード節を設けて例外を未然に防ぐ方がパフォーマンス面でも優れています。

まとめると、StartsWithを安全に使うためには以下のポイントを押さえてください。

  • 対象文字列と比較文字列のnullチェックを必ず行う
  • 空文字列の扱いに注意し、必要に応じて特別処理を行う
  • ガード節を設けて例外発生を未然に防ぐ

これらを実践することで、例外によるアプリケーションの異常終了や予期せぬ動作を防げます。

最新C#機能との組み合わせ

Nullable Reference Types

C# 8.0で導入されたNullable Reference Types(NRT)は、参照型変数がnullを許容するかどうかを明示的に示す機能です。

これにより、null参照による例外をコンパイル時に検出しやすくなります。

StartsWithメソッドを使う際、対象の文字列や比較文字列がnullである可能性がある場合、NRTを活用して安全に扱うことができます。

#nullable enable
using System;
class Program
{
    static void Main()
    {
        string? source = "HelloWorld";
        string? prefix = null;
        // コンパイラはprefixがnullの可能性を警告する
        if (source != null && prefix != null && source.StartsWith(prefix))
        {
            Console.WriteLine("前方一致しました。");
        }
        else
        {
            Console.WriteLine("前方一致しません。");
        }
    }
}
前方一致しません。

この例では、string?null許容型を宣言し、nullチェックを行うことで安全にStartsWithを呼び出しています。

NRTを有効にすると、nullの可能性がある変数に対して警告が出るため、早期に問題を発見しやすくなります。

パターンマッチングと when

C# 7.0以降のパターンマッチング機能を使うと、switch文やif文で条件付きの型チェックや値チェックを簡潔に記述できます。

whenキーワードを使うと、さらに細かい条件を付けられます。

StartsWithを使った前方一致判定と組み合わせることで、コードの可読性と保守性が向上します。

using System;
class Program
{
    static void Main()
    {
        object? input = "CSharpProgramming";
        if (input is string s && s.StartsWith("CSharp", StringComparison.OrdinalIgnoreCase))
        {
            Console.WriteLine("CSharpで始まっています。");
        }
        else
        {
            Console.WriteLine("条件に一致しません。");
        }
        // switch式でのパターンマッチングとwhen
        switch (input)
        {
            case string str when str.StartsWith("CSharp"):
                Console.WriteLine("switch: CSharpで始まっています。");
                break;
            default:
                Console.WriteLine("switch: 条件に一致しません。");
                break;
        }
    }
}
CSharpで始まっています。
switch: CSharpで始まっています。

このように、パターンマッチングとwhenを使うことで、型チェックと条件判定を一箇所で行い、冗長なコードを減らせます。

Raw String Literals での定義

C# 11で導入されたRaw String Literalsは、複数行の文字列やエスケープシーケンスを気にせずに記述できる機能です。

前方一致判定で使う文字列リテラルを見やすく、メンテナンスしやすくできます。

using System;
class Program
{
    static void Main()
    {
        string source = "This is a sample text.";
        // Raw String Literalで複数行文字列を定義
        string prefix = """
                        This is
                        """;
        bool isMatch = source.StartsWith(prefix.Trim(), StringComparison.Ordinal);
        Console.WriteLine(isMatch); // true
    }
}
True

Raw String Literalsは、複雑な文字列や複数行の文字列をそのまま記述できるため、正規表現パターンやJSON、XMLなどの文字列を扱う際に特に便利です。

StartsWithの引数としても使いやすく、コードの可読性が向上します。

まとめ

C#のStartsWithメソッドを使った文字列の前方一致判定は、多様な比較オプションや最新機能と組み合わせることで、正確かつ高速に実装できます。

StringComparisonの適切な選択やカルチャの固定、ReadOnlySpan<char>の活用でパフォーマンスを最適化しつつ、例外対策や最新の言語機能を活用することで安全で保守性の高いコードが書けます。

大規模データ処理や並列化も視野に入れた実践的なテクニックが理解できます。

関連記事

Back to top button
目次へ