文字列

【C#】String.ReplaceとRegexでマスターする文字列置換の基本と応用テクニック

C#で部分文字列を入れ替えるなら、まず文字列は不変なのでReplaceは新しい文字列を返す点を押さえておくと安心です。

単純な置換はString.Replace、大文字小文字を無視したりパターンで抜く場合はRegex.Replaceを使うと効率的です。

C#で選ぶ文字列置換メソッド

C#で文字列の置換を行う際には、主にString.ReplaceメソッドとRegex.Replaceメソッドの2つがよく使われます。

それぞれの特徴や使いどころを理解することで、効率的かつ適切な文字列操作が可能になります。

ここでは、この2つのメソッドの違いと、どのようなシナリオで使い分けるべきかを詳しく解説します。

String.ReplaceとRegex.Replaceの違い

String.ReplaceRegex.Replaceはどちらも文字列の置換を行うメソッドですが、その内部の仕組みや用途には大きな違いがあります。

基本的な動作の違い

  • String.Replace

String.Replaceは単純な文字列の置換に特化しています。

置換対象は固定の文字列であり、部分一致した文字列をすべて指定した別の文字列に置き換えます。

大文字・小文字の区別は厳密に行われ、部分的なパターンマッチングや正規表現はサポートしていません。

  • Regex.Replace

Regex.Replaceは正規表現を使ったパターンマッチングに基づく置換を行います。

複雑なパターンや条件にマッチした部分を置換できるため、単純な文字列置換では対応できない高度な置換処理に適しています。

大文字・小文字の区別を無視した置換や、キャプチャグループを使った動的な置換も可能です。

パフォーマンスの違い

  • String.Replaceは単純な文字列置換のため、処理が軽く高速です。特に置換対象が固定文字列で、置換回数が少ない場合に最適です
  • Regex.Replaceは正規表現の解析やマッチング処理が必要なため、String.Replaceに比べて処理コストが高くなります。大量の文字列や頻繁な置換処理ではパフォーマンスに注意が必要です

使いやすさと柔軟性

  • String.Replaceはシンプルで直感的に使えます。コードも読みやすく、単純な置換処理には最適です
  • Regex.Replaceは正規表現の知識が必要ですが、複雑なパターンや条件付きの置換が可能です。例えば、数字だけを置換したり、特定の単語の前後の文字を条件に置換したりできます

置換の対象

特徴String.ReplaceRegex.Replace
置換対象固定の文字列正規表現パターン
大文字・小文字区別ありオプションで無視可能
パターンの柔軟性なしあり
置換の動的制御なしMatchEvaluatorで可能
パフォーマンス高速やや低速

選択基準とシナリオ別活用ポイント

どちらのメソッドを使うべきかは、置換したい文字列の性質や処理の要件によって変わります。

ここでは、具体的な選択基準と代表的なシナリオを紹介します。

単純な文字列置換ならString.Replace

  • 選択基準
    • 置換対象が固定の文字列である
    • 大文字・小文字の区別が必要
    • パフォーマンスを重視する
    • 置換処理が単純でコードの可読性を優先したい
  • シナリオ例
    • ユーザー入力の特定キーワードを別の単語に置き換える
    • ファイルパスの区切り文字を統一する(例:\/に置換)
    • 定型文の一部を差し替える
using System;
class Program
{
    static void Main()
    {
        string original = "C#は素晴らしい言語です。C#を学びましょう。";
        // "C#"を"シーシャープ"に置換
        string replaced = original.Replace("C#", "シーシャープ");
        Console.WriteLine(replaced);
    }
}
シーシャープは素晴らしい言語です。シーシャープを学びましょう。

この例では、String.Replaceを使って単純に「C#」を「シーシャープ」に置換しています。

大文字・小文字の区別があり、正規表現は不要なケースです。

大文字・小文字を区別しない置換や複雑なパターンはRegex.Replace

  • 選択基準
    • 大文字・小文字を区別せずに置換したい
    • 数字や特定の文字種をまとめて置換したい
    • 複雑なパターンマッチングが必要
    • 置換内容を動的に変えたい(MatchEvaluatorを使う)
  • シナリオ例
    • ユーザーの入力テキストから特定の単語を大文字・小文字問わず置換
    • 電話番号やメールアドレスのフォーマットを検出して置換
    • ログファイルの特定パターンを抽出・置換
    • HTMLタグの一部を置換
using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string text = "Error: File not found. error: Access denied.";
        string pattern = "error";
        string replacement = "警告";
        // 大文字・小文字を区別せずに置換
        Regex regex = new Regex(pattern, RegexOptions.IgnoreCase);
        string result = regex.Replace(text, replacement);
        Console.WriteLine(result);
    }
}
警告: File not found. 警告: Access denied.

この例では、Regex.Replaceを使い、大文字・小文字を区別せずに「error」を「警告」に置換しています。

String.Replaceでは対応できないケースです。

動的な置換処理が必要な場合

Regex.ReplaceMatchEvaluatorデリゲートを使うことで、マッチした文字列に応じて置換内容を動的に決定できます。

例えば、数字を検出してその値を2倍に置換する場合などです。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "価格は100円、送料は200円です。";
        string pattern = @"\d+";
        string result = Regex.Replace(input, pattern, match =>
        {
            int value = int.Parse(match.Value);
            return (value * 2).ToString();
        });
        Console.WriteLine(result);
    }
}
価格は200円、送料は400円です。

このように、単純な文字列置換では対応できない複雑な処理もRegex.Replaceなら実現できます。

パフォーマンスを重視する場合の注意点

  • 置換対象が単純な文字列であれば、String.Replaceを優先してください
  • 正規表現を使う場合は、パターンをコンパイル済みRegexOptions.Compiledにすることで高速化が期待できますが、初回のコンパイルコストがかかります
  • 頻繁に大量の置換を行う場合は、パフォーマンス計測を行い、適切なメソッドを選択してください
シナリオ・条件推奨メソッド理由
単純な文字列の置換String.Replaceシンプルで高速、コードも読みやすい
大文字・小文字を区別しない置換Regex.ReplaceRegexOptions.IgnoreCaseで対応可能
複雑なパターンマッチングが必要Regex.Replace正規表現の柔軟性を活かせる
置換内容を動的に決定したいRegex.ReplaceMatchEvaluatorで動的置換が可能
パフォーマンス重視で単純な置換String.Replace正規表現より高速

このように、String.ReplaceRegex.Replaceはそれぞれ得意分野が異なります。

置換したい文字列の性質や処理の要件に応じて、適切なメソッドを選択してください。

そうすることで、コードの可読性やパフォーマンスを両立できます。

String.Replaceの基本操作

メソッドシグネチャ

String.Replaceメソッドは、C#のSystem.Stringクラスに用意されている文字列置換の基本メソッドです。

主に2つのオーバーロードが存在します。

  1. 文字列を置換するオーバーロード
public string Replace(string oldValue, string newValue);
  • oldValue:置換対象の文字列。空文字列やnullは例外の原因になるため注意が必要です
  • newValue:置換後の文字列。nullを指定すると空文字列として扱われます
  1. 文字を置換するオーバーロード
public string Replace(char oldChar, char newChar);
  • oldChar:置換対象の文字
  • newChar:置換後の文字

どちらのメソッドも元の文字列を変更せず、新しい文字列を返します。

元の文字列は不変(イミュータブル)であるため、置換結果は新しいインスタンスとして生成されます。

例:文字列置換の基本

using System;
class Program
{
    static void Main()
    {
        string original = "Hello World!";
        // "World"を"C#"に置換
        string replaced = original.Replace("World", "C#");
        Console.WriteLine(replaced);
    }
}
Hello C#!

例:文字置換の基本

using System;
class Program
{
    static void Main()
    {
        string original = "banana";
        // 'a'を'o'に置換
        string replaced = original.Replace('a', 'o');
        Console.WriteLine(replaced);
    }
}
bonono

文字列の不変性とメモリ

String.Replaceは元の文字列を直接変更しません。

C#のstring型は不変(immutable)であり、一度生成された文字列は変更できない仕様です。

そのため、Replaceメソッドを呼び出すと、置換後の新しい文字列がヒープ上に生成されます。

この不変性には以下のような影響があります。

  • メモリ消費

置換処理のたびに新しい文字列が生成されるため、大量の置換を繰り返すとメモリ使用量が増加します。

特に長い文字列や多数の置換を行う場合は注意が必要です。

  • スレッドセーフ

文字列が不変であるため、複数スレッドから同じ文字列を参照しても安全です。

Replaceは新しい文字列を返すため、元の文字列は変更されません。

  • パフォーマンス

置換回数が多い場合は、StringBuilderを使った置換やSpan<char>を活用した方法を検討すると効率的です。

メモリ消費のイメージ

操作内容メモリの動き
string s = "abcabc";文字列リテラルがメモリに確保される
s.Replace("a", "x");新しい文字列 "xbcxbc" が生成される
元の文字列は変更されない元の "abcabc" はそのまま残る

文字コードとカルチャ依存

String.Replaceは文字列の置換を行う際に、文字コード(Unicode)に基づく厳密な比較を行います。

つまり、置換対象の文字列と完全に一致する部分だけを置換します。

カルチャ(文化圏)に依存した比較は行いません。

文字コードによる比較

  • 置換対象の文字列はバイト列ではなくUnicodeコードポイント単位で比較されます
  • 大文字・小文字の区別は厳密に行われるため、"a""A"は異なる文字として扱われます

カルチャ依存の影響

String.Replaceはカルチャ依存の比較をしないため、例えばトルコ語のiİのような特殊な大文字・小文字変換は考慮されません。

大文字・小文字を区別しない置換が必要な場合は、Regex.Replaceなどの別手段を使う必要があります。

例:大文字・小文字の区別

using System;
class Program
{
    static void Main()
    {
        string text = "Apple apple APPLE";
        // "apple"を"orange"に置換(大文字小文字区別あり)
        string replaced = text.Replace("apple", "orange");
        Console.WriteLine(replaced);
    }
}
Apple orange APPLE

この例では、小文字の”apple”だけが置換され、大文字の”Apple”や”APPLE”は置換されません。

  • String.ReplaceはUnicodeコードポイント単位で厳密に比較します
  • 大文字・小文字の区別は常に行われます
  • カルチャ依存の比較は行わないため、文化圏による特殊な文字変換は考慮されません

この特性を理解して使うことで、意図しない置換ミスを防げます。

String.Replaceの応用例

複数トークンの一括置換

String.Replaceは単一の文字列や文字の置換に特化していますが、複数の異なるトークンを一括で置換したい場合もよくあります。

String.Replaceは一度に1つの置換しかできないため、複数トークンの置換は連続して呼び出す形で実装します。

連続呼び出しによる複数置換

複数の置換対象と置換後の文字列を用意し、順番にReplaceを呼び出す方法です。

using System;
class Program
{
    static void Main()
    {
        string text = "今日は{year}年{month}月{day}日です。";
        // 複数のトークンを置換
        string replaced = text
            .Replace("{year}", "2024")
            .Replace("{month}", "06")
            .Replace("{day}", "15");
        Console.WriteLine(replaced);
    }
}
今日は2024年06月15日です。

この例では、{year}, {month}, {day}という3つのトークンをそれぞれ対応する値に置換しています。

Replaceを連続で呼び出すことで複数のトークンを一括で置換できます。

配列や辞書を使った動的な複数置換

複数の置換対象が動的に決まる場合は、辞書Dictionary<string, string>を使ってループ処理で置換する方法が便利です。

using System;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        string template = "名前: {name}, 年齢: {age}, 職業: {job}";
        var replacements = new Dictionary<string, string>
        {
            { "{name}", "山田太郎" },
            { "{age}", "30" },
            { "{job}", "エンジニア" }
        };
        string result = template;
        foreach (var pair in replacements)
        {
            result = result.Replace(pair.Key, pair.Value);
        }
        Console.WriteLine(result);
    }
}
名前: 山田太郎, 年齢: 30, 職業: エンジニア

この方法は置換対象が増減しても柔軟に対応でき、テンプレート文字列の置換に適しています。

注意点

  • 置換対象の文字列が重複している場合、置換順序によって結果が変わることがあります。例えば、"abc""ab"を置換する場合は、長い文字列から置換するのが安全です
  • 置換対象が多い場合はパフォーマンスに注意してください。大量の連続置換は処理時間が増加します

書式指定と文字列補間の組み合わせ

String.Replaceは単純な文字列置換ですが、C#には文字列の書式指定や文字列補間機能もあります。

これらを組み合わせることで、より柔軟で読みやすい文字列生成が可能です。

文字列補間とReplaceの組み合わせ例

文字列補間$""でベースの文字列を作成し、さらにReplaceで特定のトークンを置換するパターンです。

using System;
class Program
{
    static void Main()
    {
        string name = "佐藤花子";
        string date = "2024/06/15";
        string template = $"こんにちは、{name}さん。今日は{date}です。";
        // さらにテンプレート内の「さん」を「様」に置換
        string result = template.Replace("さん", "様");
        Console.WriteLine(result);
    }
}
こんにちは、佐藤花子様。今日は2024/06/15です。

この例では、文字列補間で変数を埋め込みつつ、Replaceで特定の文字列を置換しています。

補間と置換を組み合わせることで、柔軟な文字列操作が可能です。

書式指定を使った数値や日付の整形

String.Replaceは文字列の置換に特化しているため、数値や日付の書式指定は文字列補間やString.Formatで行い、その後にReplaceを使うのが一般的です。

using System;
class Program
{
    static void Main()
    {
        int price = 12345;
        DateTime date = new DateTime(2024, 6, 15);
        string template = "価格は{price}円、日付は{date}です。";
        string formatted = template
            .Replace("{price}", $"{price:N0}")  // 3桁区切りの数値書式
            .Replace("{date}", date.ToString("yyyy年MM月dd日"));
        Console.WriteLine(formatted);
    }
}
価格は12,345円、日付は2024年06月15日です。

この例では、Replaceの前に数値や日付を適切な書式で文字列化し、テンプレートのトークンを置換しています。

書式指定と置換を組み合わせることで、見やすく整った文字列を生成できます。

  • 複数トークンの置換はReplaceを連続で呼び出すか、辞書を使ってループ処理で行います
  • 文字列補間や書式指定と組み合わせることで、より柔軟で読みやすい文字列生成が可能です
  • 数値や日付の書式はReplaceの前に文字列化しておくと便利

Regex.Replaceの基礎

正規表現エンジンの動作概要

C#のRegex.Replaceは、正規表現パターンにマッチした部分を置換するメソッドです。

正規表現エンジンは、入力文字列に対して指定されたパターンを解析し、マッチする箇所を検出します。

検出されたマッチ部分に対して置換処理を行い、新しい文字列を返します。

正規表現エンジンの主な処理の流れは以下の通りです。

  1. パターンの解析

正規表現パターン文字列を解析し、内部的な状態マシン(NFAやDFA)を構築します。

  1. 入力文字列の走査

文字列の先頭から順にパターンにマッチする部分を探します。

  1. マッチの検出

パターンに合致する部分が見つかると、その範囲を記録します。

  1. 置換処理

マッチした部分を指定された置換文字列やMatchEvaluatorの戻り値で置き換えます。

  1. 次のマッチ探索

置換後の文字列の残り部分で再度マッチを探し、すべてのマッチを置換します。

この処理は、複雑なパターンやオプション設定によって挙動が変わるため、正規表現の理解とオプションの適切な設定が重要です。

オプション設定

Regex.Replaceでは、正規表現の動作を制御するためにRegexOptions列挙体を使ってオプションを指定できます。

代表的なオプションを解説します。

IgnoreCase

RegexOptions.IgnoreCaseは、大文字・小文字を区別せずにマッチングを行うオプションです。

英字の大文字・小文字を区別しない置換をしたい場合に使います。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "Hello hello HELLO";
        string pattern = "hello";
        string replacement = "Hi";
        Regex regex = new Regex(pattern, RegexOptions.IgnoreCase);
        string result = regex.Replace(input, replacement);
        Console.WriteLine(result);
    }
}
Hi Hi Hi

この例では、helloの大文字・小文字の違いを無視してすべて置換しています。

Multiline

RegexOptions.Multilineは、複数行の文字列に対して^$の意味を変えるオプションです。

  • 通常、^は文字列の先頭、$は文字列の末尾にマッチします
  • Multilineを指定すると、各行の先頭と末尾にもマッチするようになります
using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "first line\nsecond line\nthird line";
        string pattern = "^\\w+";
        Regex regex = new Regex(pattern, RegexOptions.Multiline);
        var matches = regex.Matches(input);
        foreach (Match match in matches)
        {
            Console.WriteLine(match.Value);
        }
    }
}
first
second
third

この例では、各行の先頭の単語を抽出しています。

Multilineがないと最初の行の先頭だけにマッチします。

Singleline

RegexOptions.Singlelineは、.(ドット)が改行文字にもマッチするようにするオプションです。

  • 通常、.は改行文字を除く任意の1文字にマッチします
  • Singlelineを指定すると、改行も含めて任意の1文字にマッチします
using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "abc\ndef";
        string pattern = "a.*f";
        Regex regexWithoutSingleline = new Regex(pattern);
        Regex regexWithSingleline = new Regex(pattern, RegexOptions.Singleline);
        Console.WriteLine("Without Singleline: " + regexWithoutSingleline.IsMatch(input));
        Console.WriteLine("With Singleline: " + regexWithSingleline.IsMatch(input));
    }
}
Without Singleline: False
With Singleline: True

この例では、改行をまたぐマッチをSinglelineオプションで可能にしています。

ExplicitCapture

RegexOptions.ExplicitCaptureは、名前付きキャプチャグループや通常のキャプチャグループを明示的に指定しない限り、キャプチャを行わないオプションです。

  • 通常、丸括弧()はキャプチャグループとして扱われます
  • ExplicitCaptureを指定すると、()はキャプチャしなくなり、キャプチャしたい場合は(?<name>...)のように名前付きグループを使う必要があります
using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "abc123def";
        string pattern = "(abc)(123)(def)";
        Regex regexDefault = new Regex(pattern);
        Regex regexExplicit = new Regex(pattern, RegexOptions.ExplicitCapture);
        Console.WriteLine("Default group count: " + regexDefault.Match(input).Groups.Count);
        Console.WriteLine("ExplicitCapture group count: " + regexExplicit.Match(input).Groups.Count);
    }
}
Default group count: 4
ExplicitCapture group count: 1

この例では、ExplicitCaptureを指定すると、丸括弧のグループはキャプチャされず、全体マッチのみがキャプチャされます。

マッチタイムアウトの設定

正規表現は複雑なパターンや長い文字列に対して処理時間が長くなることがあります。

特に悪意のある入力や誤ったパターンで「バックトラッキング爆発」が起きると、処理が極端に遅くなることがあります。

C#のRegexクラスでは、マッチ処理にタイムアウトを設定して、一定時間を超えた場合に例外をスローする機能があります。

これにより、無限ループや長時間の処理を防止できます。

タイムアウトの指定方法

RegexのコンストラクタでTimeSpan型のタイムアウトを指定します。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa!";
        string pattern = "(a+)+!";
        try
        {
            Regex regex = new Regex(pattern, RegexOptions.None, TimeSpan.FromMilliseconds(100));
            bool isMatch = regex.IsMatch(input);
            Console.WriteLine("マッチ結果: " + isMatch);
        }
        catch (RegexMatchTimeoutException ex)
        {
            Console.WriteLine("正規表現のマッチ処理がタイムアウトしました。");
        }
    }
}
正規表現のマッチ処理がタイムアウトしました。

この例では、意図的にバックトラッキングが多発するパターンを使い、100ミリ秒のタイムアウトを設定しています。

処理が長引くとRegexMatchTimeoutExceptionが発生します。

注意点

  • タイムアウトはRegexインスタンスごとに設定します
  • Regex.Replaceのオーバーロードにもタイムアウトを指定できるものがあります
  • タイムアウトを設定しない場合、長時間の処理が発生するリスクがあります

適切なタイムアウト設定は、安定したアプリケーション動作に役立ちます。

Regex.Replaceの応用テクニック

キャプチャグループの再利用

正規表現のキャプチャグループは、マッチした部分文字列を抽出し、置換時に再利用できる強力な機能です。

Regex.Replaceでは、置換文字列内でキャプチャグループを参照することで、マッチした内容を活かした置換が可能です。

名前付きグループ

名前付きグループは、キャプチャグループにわかりやすい名前を付けて管理できる機能です。

(?<name>...)の形式で定義し、置換文字列では${name}で参照します。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "2024-06-15";
        string pattern = @"(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})";
        // 年月日を「年/月/日」形式に置換
        string replaced = Regex.Replace(input, pattern, "${year}年${month}月${day}日");
        Console.WriteLine(replaced);
    }
}
2024年06月15日

この例では、yearmonthdayという名前付きグループを使い、マッチした年月日を日本語表記に置換しています。

名前付きグループは複数のグループを管理しやすく、可読性が高いです。

順序付きグループ

名前を付けずに丸括弧で囲んだグループは順序付きグループとして扱われます。

置換文字列では$1$2$3のように番号で参照します。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "John Smith";
        string pattern = @"(\w+) (\w+)";
        // 「姓, 名」の形式に置換
        string replaced = Regex.Replace(input, pattern, "$2, $1");
        Console.WriteLine(replaced);
    }
}
Smith, John

この例では、1番目のグループが名、2番目のグループが姓としてマッチし、置換時に順序付きグループを使って入れ替えています。

動的置換ロジック

MatchEvaluatorデリゲート

Regex.Replaceは、置換文字列の代わりにMatchEvaluatorデリゲートを受け取るオーバーロードがあります。

これにより、マッチした内容に応じて動的に置換文字列を生成できます。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "価格は100円、送料は200円です。";
        string pattern = @"\d+";
        string replaced = Regex.Replace(input, pattern, match =>
        {
            int value = int.Parse(match.Value);
            // 数値を2倍にして置換
            return (value * 2).ToString();
        });
        Console.WriteLine(replaced);
    }
}
価格は200円、送料は400円です。

この例では、数字を検出して2倍に変換し、動的に置換しています。

MatchEvaluatorを使うことで、単純な文字列置換ではできない複雑な処理が可能です。

部分一致と全一致の違い

MatchEvaluatorはマッチした部分文字列単位で呼び出されます。

つまり、部分一致ごとに処理が行われ、全体文字列の一括置換ではありません。

  • 部分一致:マッチした各部分に対して個別に置換処理を行います
  • 全一致:文字列全体に対して一度だけ置換処理を行う(MatchEvaluatorではなく単純な置換文字列の場合)

部分一致の特性を活かし、マッチごとに異なる処理や条件分岐を実装できます。

コンパイル済みパターンで高速化

正規表現のパターンは、初回のマッチ時に解析・コンパイルされます。

頻繁に同じパターンを使う場合、RegexOptions.Compiledを指定して事前にコンパイル済みの正規表現オブジェクトを作成すると、パフォーマンスが向上します。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "abc123def456ghi789";
        string pattern = @"\d+";
        Regex regex = new Regex(pattern, RegexOptions.Compiled);
        string replaced = regex.Replace(input, "#");
        Console.WriteLine(replaced);
    }
}
abc#def#ghi#

注意点

  • RegexOptions.Compiledは初回のコンパイルに時間がかかるため、使い捨てのパターンには向きません
  • 長時間実行されるアプリケーションや大量の置換処理で効果を発揮します
  • コンパイル済みのRegexオブジェクトは再利用可能で、スレッドセーフです

このように、コンパイル済みパターンを活用することで、正規表現のパフォーマンスを最適化できます。

パフォーマンス視点の比較

文字列長と処理時間

文字列置換のパフォーマンスは、対象となる文字列の長さに大きく影響されます。

一般的に、文字列が長くなるほど処理時間は増加しますが、String.ReplaceRegex.Replaceではその増加の仕方に違いがあります。

  • String.Replace

単純な文字列置換であるため、対象文字列の長さに比例して処理時間が増えます。

置換対象の文字列が短く、置換回数が少なければ高速に動作します。

内部的には文字列を走査し、マッチした部分を新しい文字列に置き換えます。

  • Regex.Replace

正規表現エンジンによるパターンマッチングが必要なため、String.Replaceよりも処理時間が長くなる傾向があります。

特に複雑なパターンや多くのマッチがある場合は、処理時間が大幅に増加します。

実測例(概算)

文字列長 (文字数)String.Replace 処理時間 (ms)Regex.Replace 処理時間 (ms)
1,00015
10,0001050
100,000100500

この表はあくまで目安ですが、Regex.Replaceは正規表現の解析やマッチング処理が加わるため、String.Replaceの数倍の時間がかかることが多いです。

メモリ確保の最小化

文字列は不変(immutable)であるため、置換処理では新しい文字列が生成されます。

大量の置換や長い文字列の置換では、メモリ確保がパフォーマンスのボトルネックになることがあります。

  • String.Replace

置換対象が単純な文字列の場合、内部で効率的に新しい文字列を生成しますが、置換回数が多いとその分メモリ確保が増えます。

連続して複数回Replaceを呼ぶと、そのたびに新しい文字列が生成されるため、メモリ使用量が増加します。

  • Regex.Replace

正規表現エンジンはマッチした部分だけを置換し、内部でバッファを使って効率的に文字列を構築します。

ただし、複雑なパターンや大量のマッチがあるとメモリ使用量が増えることがあります。

メモリ使用量削減のポイント

  • 連続置換はできるだけまとめて行います
  • 大量の置換が必要な場合はStringBuilderSpan<char>を活用します
  • RegexOptions.Compiledを使い、正規表現の再解析コストを削減します

Span APIによる高速化

C# 7.2以降で導入されたSpan<T>は、メモリのコピーを伴わずに文字列や配列の部分を参照できる構造体です。

これを活用すると、文字列置換のパフォーマンスを大幅に向上させることが可能です。

Spanを使った置換のメリット

  • メモリコピーの削減

文字列の部分を直接参照できるため、新しい文字列を生成する際のコピーコストを減らせます。

  • GC負荷の軽減

ヒープ割り当てを減らし、ガベージコレクションの負荷を抑えられます。

  • 高速な部分文字列操作

文字列のスライスや検索を効率的に行えます。

例:Span<char>を使った簡易的な置換処理

using System;
class Program
{
    static void Main()
    {
        string input = "Hello World! Hello C#!";
        ReadOnlySpan<char> span = input.AsSpan();
        string target = "Hello";
        string replacement = "Hi";
        int index = span.IndexOf(target);
        if (index < 0)
        {
            Console.WriteLine(input);
            return;
        }
        Span<char> buffer = stackalloc char[input.Length - target.Length + replacement.Length];
        int pos = 0;
        // 置換前の部分をコピー
        span.Slice(0, index).CopyTo(buffer.Slice(pos));
        pos += index;
        // 置換文字列をコピー
        replacement.AsSpan().CopyTo(buffer.Slice(pos));
        pos += replacement.Length;
        // 置換後の残りをコピー
        span.Slice(index + target.Length).CopyTo(buffer.Slice(pos));
        pos += span.Length - (index + target.Length);
        string result = new string(buffer.Slice(0, pos));
        Console.WriteLine(result);
    }
}
Hi World! Hello C#!

この例では、Span<char>を使って文字列の一部を効率的に置換しています。

stackallocでスタック上にバッファを確保し、メモリ割り当てを最小限に抑えています。

注意点

  • Span<T>はスタック上のメモリを扱うため、長い文字列の処理には向かない場合があります
  • 複雑な置換処理には専用のライブラリやRegexの利用が適しています
  • .NETのバージョンや環境によってはSpan<T>のサポート状況に差があります

パフォーマンスを重視する場合は、文字列の長さや置換の複雑さに応じてString.ReplaceRegex.ReplaceSpan<T>を使い分けることが重要です。

適切な手法を選ぶことで、処理時間やメモリ使用量を最適化できます。

例外とエラーへの対処

Nullまたは空文字の扱い

String.ReplaceRegex.Replaceを使う際、引数にnullや空文字列を渡すと例外が発生したり、意図しない動作になることがあります。

これらのケースを適切に扱うことが重要です。

String.Replaceの場合

  • oldValuenullを渡すとArgumentNullExceptionが発生します
  • newValuenullを渡すと、空文字列として扱われ、oldValueが削除される形になります
  • 空文字列は有効な引数ですが、oldValueに空文字列を指定すると例外が発生します
using System;
class Program
{
    static void Main()
    {
        string text = "Hello World";
        try
        {
            // oldValueにnullを渡すと例外
            string result = text.Replace(null, "Test");
        }
        catch (ArgumentNullException ex)
        {
            Console.WriteLine("例外発生: " + ex.Message);
        }
        // newValueにnullを渡すと削除扱い
        string removed = text.Replace("World", null);
        Console.WriteLine(removed);
    }
}
例外発生: 値は null であってはなりません。 (パラメーター名: oldValue)
Hello

Regex.Replaceの場合

  • パターンや置換文字列にnullを渡すとArgumentNullExceptionが発生します
  • 空文字列は有効ですが、パターンが空文字列の場合はすべての位置にマッチし、意図しない結果になることがあります
using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "abc";
        try
        {
            // パターンにnullを渡すと例外
            string result = Regex.Replace(input, null, "X");
        }
        catch (ArgumentNullException ex)
        {
            Console.WriteLine("例外発生: " + ex.Message);
        }
        // 空文字列パターンはすべての位置にマッチ
        string replaced = Regex.Replace(input, "", "-");
        Console.WriteLine(replaced);
    }
}
例外発生: 値は null であってはなりません。 (パラメーター名: pattern)
-a-b-c-

対策

  • 引数にnullが渡る可能性がある場合は、事前にnullチェックを行います
  • 空文字列のパターンは意図的に使う場合を除き避けます
  • 例外をキャッチして適切に処理します

不正パターンの検出

Regexのパターンが不正(無効な正規表現)である場合、ArgumentExceptionがスローされます。

パターンの構文エラーや不正な文字列が原因です。

例:不正な正規表現パターン

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "test";
        string invalidPattern = @"(abc"; // 括弧の閉じ忘れ
        try
        {
            string result = Regex.Replace(input, invalidPattern, "X");
        }
        catch (ArgumentException ex)
        {
            Console.WriteLine("正規表現エラー: " + ex.Message);
        }
    }
}
正規表現エラー: Invalid pattern '(abc' at offset 4. Not enough )'s.

対策

  • 正規表現パターンは信頼できるソースから取得します
  • ユーザー入力など動的にパターンを生成する場合は、事前にRegex.IsMatchRegex.TryParse(.NET 7以降)で検証します
  • 例外をキャッチしてエラーメッセージを表示したり、代替処理を行います

RegexMatchTimeoutException

正規表現のマッチ処理が指定したタイムアウト時間を超えた場合、RegexMatchTimeoutExceptionがスローされます。

これは、複雑なパターンや長い文字列でバックトラッキングが多発し、処理が長時間かかることを防ぐための安全機構です。

例:タイムアウト例外の発生

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = new string('a', 10000) + "!";
        string pattern = "(a+)+!";
        try
        {
            Regex regex = new Regex(pattern, RegexOptions.None, TimeSpan.FromMilliseconds(1));
            bool isMatch = regex.IsMatch(input);
            Console.WriteLine("マッチ結果: " + isMatch);
        }
        catch (RegexMatchTimeoutException ex)
        {
            Console.WriteLine("タイムアウト例外が発生しました。");
        }
    }
}
タイムアウト例外が発生しました。

対策

  • RegexのコンストラクタやRegex.Replaceのオーバーロードで適切なタイムアウトを設定します
  • 複雑なパターンは見直し、可能な限りシンプルにします
  • 入力文字列の長さや内容を制限し、悪意のある入力を防ぐ
  • 例外をキャッチして処理を中断し、ユーザーに通知します

これらの例外やエラーを適切に扱うことで、安定した文字列置換処理を実現できます。

特に正規表現を使う場合は、パターンの妥当性やタイムアウト設定に注意してください。

実用シナリオ別レシピ

日付書式の統一

異なる形式で表現された日付を統一した書式に変換するケースはよくあります。

Regex.Replaceを使うと、複数のパターンにマッチさせて一括で変換できます。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "日付は2024/6/15、または2024-06-15、さらに2024.06.15です。";
        // 年/月/日、年-月-日、年.月.日のパターンにマッチ
        string pattern = @"(?<year>\d{4})[\/\-.](?<month>\d{1,2})[\/\-.](?<day>\d{1,2})";
        string result = Regex.Replace(input, pattern, match =>
        {
            int year = int.Parse(match.Groups["year"].Value);
            int month = int.Parse(match.Groups["month"].Value);
            int day = int.Parse(match.Groups["day"].Value);
            // yyyy年MM月dd日形式に統一
            return $"{year}{month:D2}{day:D2}日";
        });
        Console.WriteLine(result);
    }
}
日付は2024年06月15日、または2024年06月15日、さらに2024年06月15日です。

この例では、スラッシュ、ハイフン、ドットで区切られた日付を正規表現で検出し、MatchEvaluatorで日本語表記の統一書式に変換しています。

機密情報のマスキング

ログやテキストに含まれる電話番号やメールアドレスなどの機密情報をマスキング(伏せ字)する際に、Regex.Replaceが役立ちます。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string input = "連絡先は090-1234-5678、メールはexample@example.comです。";
        // 電話番号のパターン(簡易版)
        string phonePattern = @"\d{2,4}-\d{2,4}-\d{4}";
        // メールアドレスのパターン(簡易版)
        string emailPattern = @"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}";
        string masked = Regex.Replace(input, phonePattern, "XXX-XXXX-XXXX");
        masked = Regex.Replace(masked, emailPattern, "*****@*****.***");
        Console.WriteLine(masked);
    }
}
連絡先はXXX-XXXX-XXXX、メールは*****@*****.***です。

この例では、電話番号とメールアドレスをそれぞれ正規表現で検出し、伏せ字に置換しています。

実際の運用ではパターンをより厳密にすることが望ましいです。

マルチラインログの整形

ログファイルなどのマルチラインテキストを整形し、特定のパターンを抽出・置換する場合にRegexOptions.Multilineを活用します。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string log = @"[INFO] 2024-06-15 10:00:00 処理開始
[ERROR] 2024-06-15 10:01:00 エラー発生
[INFO] 2024-06-15 10:02:00 処理終了";
        // 各行のログレベルを大文字に統一し、[ERROR]行のみ「!!!」を追加
        string pattern = @"^\\[(?<level>\w+)\\]";
        string result = Regex.Replace(log, pattern, match =>
        {
            string level = match.Groups["level"].Value.ToUpper();
            if (level == "ERROR")
            {
                return $"[{level}]!!!";
            }
            return $"[{level}]";
        }, RegexOptions.Multiline);
        Console.WriteLine(result);
    }
}
[INFO] 2024-06-15 10:00:00 処理開始
[ERROR]!!! 2024-06-15 10:01:00 エラー発生
[INFO] 2024-06-15 10:02:00 処理終了

この例では、Multilineオプションを使い、各行の先頭にあるログレベルを検出して置換しています。

ERRORレベルの行には特別なマークを付加しています。

特定タグのHTML置換

HTMLやXMLの特定タグを置換・削除したい場合、正規表現でタグを検出して置換できます。

ただし、複雑なHTML構造には専用のパーサーを使うことが推奨されます。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string html = "<div><span>テキスト</span><script>alert('XSS');</script></div>";
        // <script>タグとその中身を削除
        string pattern = @"<script.*?>.*?</script>";
        string cleaned = Regex.Replace(html, pattern, "", RegexOptions.IgnoreCase | RegexOptions.Singleline);
        Console.WriteLine(cleaned);
    }
}
<div><span>テキスト</span></div>

この例では、<script>タグとその中身を正規表現で検出し、削除しています。

Singlelineオプションで改行を含む内容もマッチさせ、IgnoreCaseで大文字小文字を無視しています。

これらのレシピは、実際の開発現場でよく遭遇する文字列置換の課題に対応するための基本的なパターンです。

用途に応じて正規表現や置換ロジックをカスタマイズして活用してください。

コードメンテナンスのヒント

拡張メソッドでの共通化

文字列置換の処理はプロジェクト内で何度も繰り返し使われることが多いため、共通化しておくとメンテナンス性が向上します。

C#の拡張メソッドを活用すると、既存のstring型に対して独自の置換ロジックを簡潔に追加できます。

拡張メソッドの例

using System;
using System.Collections.Generic;
public static class StringExtensions
{
    // 複数トークンを一括置換する拡張メソッド
    public static string ReplaceTokens(this string source, IDictionary<string, string> replacements)
    {
        if (string.IsNullOrEmpty(source) || replacements == null || replacements.Count == 0)
            return source;
        string result = source;
        foreach (var pair in replacements)
        {
            if (!string.IsNullOrEmpty(pair.Key))
            {
                result = result.Replace(pair.Key, pair.Value ?? string.Empty);
            }
        }
        return result;
    }
}

利用例

using System;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        string template = "こんにちは、{name}さん。今日は{date}です。";
        var replacements = new Dictionary<string, string>
        {
            { "{name}", "田中" },
            { "{date}", "2024年6月15日" }
        };
        string result = template.ReplaceTokens(replacements);
        Console.WriteLine(result);
    }
}
こんにちは、田中さん。今日は2024年6月15日です。

このように拡張メソッドで共通処理をまとめると、コードの重複を減らし、修正や機能追加が容易になります。

単一責任原則を保つ

コードの保守性を高めるためには、単一責任原則(Single Responsibility Principle, SRP)を意識することが重要です。

文字列置換の処理も、1つのメソッドやクラスが複数の役割を持たないように設計しましょう。

具体例

  • 置換ロジックは置換に専念し、入力の検証やログ出力は別のメソッドやクラスで行います
  • 複雑な置換条件やパターン生成は専用のクラスに切り出します
  • UIやビジネスロジックと文字列置換処理を分離します

メリット

  • テストがしやすくなります
  • バグの原因を特定しやすくなります
  • 変更の影響範囲を限定できます

コメントとドキュメント化

コードの可読性とメンテナンス性を高めるために、適切なコメントやドキュメントを残すことが欠かせません。

特に正規表現を使った置換処理は複雑になりやすいため、意図やパターンの説明を明記しましょう。

コメントのポイント

  • 正規表現パターンの意味や目的を簡潔に説明します
  • 置換のルールや例外処理の理由を記述します
  • 複雑なロジックや特殊な動作は詳細にコメントします

XMLドキュメントコメントの活用

メソッドやクラスにXML形式のドキュメントコメントを付けると、IDEの補完機能で説明が表示され、利用者にとって理解しやすくなります。

/// <summary>
/// 指定した複数のトークンを一括で置換します。
/// </summary>
/// <param name="source">置換対象の文字列</param>
/// <param name="replacements">置換するトークンと置換後の文字列の辞書</param>
/// <returns>置換後の文字列</returns>
public static string ReplaceTokens(this string source, IDictionary<string, string> replacements)
{
    // 実装
}

ドキュメント化の効果

  • チームメンバー間の認識共有がスムーズになります
  • 将来の自分や他者がコードを理解しやすくなります
  • バグ修正や機能追加の際の手戻りを減らせます

これらのヒントを意識してコードを書くことで、文字列置換処理の品質と保守性を高められます。

特にチーム開発や長期運用のプロジェクトでは重要なポイントです。

よくある落とし穴

大文字小文字問題

文字列置換でよく起こる問題の一つが、大文字・小文字の扱いです。

String.Replaceは大文字小文字を区別して置換を行うため、意図した文字列が置換されないことがあります。

例:String.Replaceの大文字小文字区別

using System;
class Program
{
    static void Main()
    {
        string text = "Hello HELLO hello";
        string replaced = text.Replace("hello", "hi");
        Console.WriteLine(replaced);
    }
}
Hello HELLO hi

この例では、小文字の”hello”だけが置換され、大文字の”Hello”や”HELLO”は置換されません。

対策

  • 大文字小文字を区別せずに置換したい場合は、Regex.Replaceを使い、RegexOptions.IgnoreCaseを指定します
  • 文字列を事前に大文字または小文字に変換してから置換する方法もあるが、元のケースを保持できないため注意が必要でしょう

Regex.Replaceで大文字小文字を無視する例

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string text = "Hello HELLO hello";
        string pattern = "hello";
        string replaced = Regex.Replace(text, pattern, "hi", RegexOptions.IgnoreCase);
        Console.WriteLine(replaced);
    }
}
hi hi hi

非BMP文字の取り扱い

UnicodeにはBMP(Basic Multilingual Plane)外の文字、いわゆる非BMP文字があります。

これらはサロゲートペアとして2つのcharで表現されるため、文字列操作で注意が必要です。

問題点

  • String.ReplaceRegexは基本的にchar単位で処理するため、非BMP文字を誤って分割してしまうことがあります
  • 置換対象が非BMP文字の一部だけにマッチすると、文字化けや不正な文字列になる可能性があります

例:非BMP文字(絵文字)の扱い

using System;
class Program
{
    static void Main()
    {
        string text = "😀😃😄"; // 絵文字3つ
        string replaced = text.Replace("😃", "😊");
        Console.WriteLine(replaced);
    }
}
😀😊😄

この例では問題なく置換できていますが、正規表現で非BMP文字を扱う場合は注意が必要です。

対策

  • 非BMP文字を扱う場合は、System.Text.Rune構造体(.NET Core 3.0以降)を使ってコードポイント単位で処理します
  • 正規表現で非BMP文字を扱う場合は、\X(Unicodeグラフェムクラスタ)をサポートするライブラリを検討します
  • 置換対象の文字列がサロゲートペアで分割されないように注意します

改行コードの不一致

Windows、Unix、Macなどの環境によって改行コードが異なるため、改行を含む文字列の置換で意図しない動作が起こることがあります。

主な改行コード

OS改行コード
Windows\r\n (CR+LF)
Unix/Linux\n (LF)
Mac (古い)\r (CR)

問題例

正規表現で改行を含むパターンを扱う際、改行コードの違いによりマッチしないことがあります。

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string text = "line1\r\nline2\nline3\rline4";
        string pattern = @"line2\r\nline3";
        bool isMatch = Regex.IsMatch(text, pattern);
        Console.WriteLine(isMatch);
    }
}
False

この例では、line2line3の間の改行コードが\r\n\rの可能性があり、\nだけを指定するとマッチしません。

対策

  • 改行コードを正規表現で柔軟に扱うために、\r?\n\r\n?のようにパターンを工夫します
  • RegexOptions.SinglelineRegexOptions.Multilineの使い分けを理解し、適切に設定します
  • 文字列の改行コードを事前に統一(例:string.Replace("\r\n", "\n"))してから処理します

改善例

using System;
using System.Text.RegularExpressions;
class Program
{
    static void Main()
    {
        string text = "line1\r\nline2\nline3\rline4";
        string pattern = @"line2\r?\nline3";
        bool isMatch = Regex.IsMatch(text, pattern);
        Console.WriteLine(isMatch);
    }
}
True

これらの落とし穴を理解し、適切に対処することで、文字列置換処理の信頼性と正確性を高められます。

特に大文字小文字の扱い、Unicodeの特殊文字、改行コードの違いは注意が必要です。

ライブラリとフレームワークの補助機能

.NETのRegexHelpers

.NET標準ライブラリには、System.Text.RegularExpressions名前空間の中で正規表現を扱うための基本的なクラスが提供されていますが、これに加えて補助的な機能を持つヘルパークラスやメソッドも存在します。

これらは正規表現の利用をより簡単かつ安全にするためのツールとして役立ちます。

RegexHelpersの概要

RegexHelpersは.NETの標準APIには直接含まれていませんが、Microsoftやコミュニティが提供するライブラリやサンプルコードの中で、正規表現の共通処理をまとめたヘルパークラスとしてよく使われています。

例えば、以下のような機能を持つことが多いです。

  • 正規表現パターンの検証
  • マッチング結果の安全な取得
  • 置換処理の共通化
  • タイムアウト設定の一元管理
  • 大文字小文字無視やマルチライン対応の簡易設定

例:簡単なRegexHelpersクラス

using System;
using System.Text.RegularExpressions;
public static class RegexHelpers
{
    // 正規表現パターンの妥当性をチェックするメソッド
    public static bool IsValidPattern(string pattern)
    {
        if (string.IsNullOrEmpty(pattern))
            return false;
        try
        {
            _ = new Regex(pattern);
            return true;
        }
        catch (ArgumentException)
        {
            return false;
        }
    }
    // 大文字小文字を無視して置換する共通メソッド
    public static string ReplaceIgnoreCase(string input, string pattern, string replacement)
    {
        if (input == null) return null;
        if (!IsValidPattern(pattern)) throw new ArgumentException("不正な正規表現パターンです。");
        return Regex.Replace(input, pattern, replacement, RegexOptions.IgnoreCase);
    }
}

利用例

using System;
class Program
{
    static void Main()
    {
        string text = "Hello hello HELLO";
        string replaced = RegexHelpers.ReplaceIgnoreCase(text, "hello", "hi");
        Console.WriteLine(replaced);
    }
}
hi hi hi

このように、RegexHelpersを使うことで、正規表現の共通処理をまとめてコードの重複を減らし、保守性を高められます。

CommunityToolkitの拡張

MicrosoftのCommunityToolkitは、.NET開発者向けに便利な拡張機能やユーティリティを提供するオープンソースのライブラリ群です。

CommunityToolkitの中には、文字列操作や正規表現に関する拡張も含まれており、日常的な開発で役立ちます。

CommunityToolkitの特徴

  • 拡張メソッドの豊富さ

文字列やコレクション、タスクなどに対する使いやすい拡張メソッドが多数用意されています。

  • パフォーマンス最適化

内部でSpan<T>ValueTaskなどの最新APIを活用し、高速かつ効率的な処理を実現。

  • クロスプラットフォーム対応

Windowsだけでなく、LinuxやmacOS、モバイル環境でも利用可能です。

文字列置換に関する拡張例

CommunityToolkitStringExtensionsには、正規表現を使った置換や検証を簡単に行うためのメソッドが含まれています。

例えば、大文字小文字を無視した置換や複数トークンの一括置換などがサポートされています。

using System;
using CommunityToolkit.Extensions;
class Program
{
    static void Main()
    {
        string text = "The quick brown fox jumps over the lazy dog.";
        // 大文字小文字を無視して"the"を"THE"に置換
        string replaced = text.ReplaceIgnoreCase("the", "THE");
        Console.WriteLine(replaced);
    }
}
THE quick brown fox jumps over THE lazy dog.

上記はイメージコードです。

実際のメソッド名や使い方はCommunityToolkitのバージョンやモジュールによって異なる場合があります。

利用のメリット

  • 標準APIよりも簡潔で直感的なコードが書けます
  • 複雑な正規表現処理をラップしてくれるため、ミスを減らせます
  • コミュニティによるメンテナンスで最新のベストプラクティスが反映されています

これらの補助機能を活用することで、正規表現や文字列置換の実装がより安全かつ効率的になります。

特に大規模プロジェクトやチーム開発では、共通のヘルパーや拡張ライブラリを導入することを検討すると良いでしょう。

まとめ

この記事では、C#の文字列置換におけるString.ReplaceRegex.Replaceの基本から応用、パフォーマンスや例外処理、実用的なシナリオまで幅広く解説しました。

用途に応じたメソッド選択や正規表現のオプション設定、動的置換のテクニック、メンテナンス性を高めるコーディングのポイントも紹介しています。

これにより、効率的で安全な文字列置換処理の実装が可能になります。

関連記事

Back to top button
目次へ