0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C#】文字列処理でハマりやすい落とし穴とパフォーマンス改善Tips【実務向け】

0
Last updated at Posted at 2026-10-14

はじめに

文字列処理は日常的に書くコードだからこそ、非効率な書き方や環境依存の挙動が見過ごされやすい領域です。ループ内の文字列連結や正規表現の書き方は、データ量が少ない開発環境では問題にならず、本番の大量データで処理時間やメモリ使用量として顕在化します。また、Shift_JISのような文字コードは.NETのバージョンによって既定の扱いが異なり、移行時に突然例外になることがあります。本記事では、実務でよく遭遇する3つの落とし穴を扱います。

前提知識 / 用語整理

  • イミュータブル: stringは一度作成すると内容を変更できない。連結や置換のたびに新しい文字列が生成される
  • StringBuilder: 内部バッファに文字列を追加していく可変の文字列構築クラス
  • バックトラッキング: 正規表現エンジンがマッチに失敗した際、別のパターンを試すために後戻りする動作
  • コードページ: Shift_JIS(932)などの旧来の文字コード体系。.NET Core以降は既定で含まれない

落とし穴1: ループ内で+=による文字列連結を繰り返す

再現コード

// ❌ ループのたびに新しい文字列が生成される
public string BuildCsv(IEnumerable<Order> orders)
{
    var csv = "";
    foreach (var order in orders)
    {
        csv += $"{order.Id},{order.Name},{order.Amount}\n";
    }
    return csv;
}

なぜ起きるか

stringはイミュータブルなため、+=のたびに「それまでの内容+追加分」を格納する新しい文字列を確保し、古い文字列は破棄されます。N件のループでは、毎回これまでの全文字をコピーするため、処理量が件数の2乗に比例して増えます。数百件のテストデータでは差が出ませんが、数万〜数十万件のCSV出力では、処理時間の増大とGCの頻発(特に大きな文字列はLOH=ラージオブジェクトヒープに確保される)として現れます。

正しい書き方

// ◎ StringBuilderでバッファに追記する
public string BuildCsv(IEnumerable<Order> orders)
{
    var sb = new StringBuilder();
    foreach (var order in orders)
    {
        sb.Append(order.Id).Append(',')
          .Append(order.Name).Append(',')
          .Append(order.Amount).Append('\n');
    }
    return sb.ToString();
}

(ただし、ループを伴わないa + b + cのような1つの式での連結は、コンパイラがstring.ConcatにまとめるためStringBuilderは不要です。出力先がファイルやネットワークの場合は、文字列に溜め込まずにStreamWriterへ直接書き込む方が、メモリ使用量の面でさらに有利です)

落とし穴2: 正規表現の壊滅的バックトラッキングと生成コスト

再現コード

// ❌ 入れ子の量指定子を含むパターン
var pattern = @"^(a+)+$";

// ❌ 呼び出しのたびにRegexインスタンスを生成
public bool IsValid(string input) =>
    new Regex(pattern).IsMatch(input);

IsValid("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!"); // 非常に長い時間がかかる

なぜ起きるか

(a+)+のように量指定子が入れ子になったパターンは、マッチに失敗する入力に対して、組み合わせ爆発的にバックトラッキングが発生します。入力の長さが少し増えるだけで処理時間が指数的に増え、1つのリクエストでCPUを占有し続けることになります。ユーザー入力を正規表現で検証するAPIでは、これを悪用されるとサービス拒否(ReDoS)につながります。また、new Regex(...)をメソッド呼び出しのたびに生成すると、パターンの解析コストが毎回かかります。

正しい書き方

// ◎ タイムアウトを設定し、Regexを再利用する
private static readonly Regex AlphaOnly = new(
    @"^[a-z]+$",                       // 入れ子の量指定子を避けたパターンにする
    RegexOptions.Compiled,
    TimeSpan.FromMilliseconds(100));   // マッチ時間の上限

public bool IsValid(string input)
{
    try
    {
        return AlphaOnly.IsMatch(input);
    }
    catch (RegexMatchTimeoutException)
    {
        return false;
    }
}
// ◎ .NET 7以降: バックトラッキングをしないエンジンを選択する
private static readonly Regex Safe = new(@"^(a+)+$", RegexOptions.NonBacktracking);
// ◎ .NET 7以降: ソースジェネレーターでコンパイル時にコードを生成する
public static partial class Validators
{
    [GeneratedRegex(@"^[a-z]+$", RegexOptions.None, matchTimeoutMilliseconds: 100)]
    public static partial Regex AlphaOnly();
}

RegexOptions.NonBacktrackingは処理時間が入力長に比例しますが、後読み・後方参照など一部の構文が使えない制限があります。外部からの入力に正規表現を適用する場合は、パターンの見直しに加えて、matchTimeoutを必ず設定してください。

落とし穴3: .NET Core以降でShift_JISが使えず例外になる

再現コード

// ❌ .NET Framework時代のコードをそのまま移行
var encoding = Encoding.GetEncoding("shift_jis");
var text = File.ReadAllText("legacy.csv", encoding);
// => ArgumentException: 'shift_jis' is not a supported encoding name.

なぜ起きるか

.NET Framework(Windows専用)ではOSのコードページを利用できましたが、.NET Core以降はクロスプラットフォームのため、既定で含まれるエンコーディングはUTF-8・UTF-16・ASCIIなどに限られます。Shift_JIS(コードページ932)のような旧来のエンコーディングは、追加のプロバイダーを登録しないと使えません。また、エンコーディングを指定せずにFile.ReadAllTextを呼ぶと、UTF-8として読み込むため、Shift_JISのファイルは文字化けします。

正しい書き方

// ◎ アプリケーション起動時に一度だけプロバイダーを登録する
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

var encoding = Encoding.GetEncoding("shift_jis");
var text = File.ReadAllText("legacy.csv", encoding);

CodePagesEncodingProviderはSystem.Text.Encoding.CodePagesパッケージに含まれます(.NET Core 3.0以降は共有フレームワークに含まれるため、通常は追加インストール不要です)。登録はProgram.csの冒頭などで1回行えば、以降のコードからEncoding.GetEncodingで利用できます。なお、shift_jisとWindows-31J(windows-31j、コードページ932)は同じコードページ932を指すため、Encoding.GetEncoding(932)のようにコードページ番号で指定しても同じエンコーディングを取得できます。

ベストプラクティスまとめ

  • ループ内で文字列を連結する場合はStringBuilderを使う(出力先がストリームなら直接書き込む)
  • 正規表現はstatic readonlyで再利用するか、[GeneratedRegex]を使う
  • 外部入力に適用する正規表現にはmatchTimeoutを設定し、入れ子の量指定子を避ける
  • .NET 7以降ではRegexOptions.NonBacktrackingの利用を検討する
  • Shift_JIS系のファイルを扱うアプリでは、起動時にCodePagesEncodingProviderを登録する
  • ファイルの読み書きでは、エンコーディングを明示的に指定する

おわりに

文字列処理の問題は、データ量や実行環境が本番に近づいて初めて表面化するものが多く、書いた時点では気づけません。連結にはStringBuilder、正規表現にはタイムアウトと再利用、レガシーな文字コードには明示的なプロバイダー登録、という3点を習慣にしておくと、運用開始後のトラブルを減らせます。

参考リンク

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?