C#を使用したアプリケーション開発において、文字列のパターンマッチングやバリデーションを行う際に欠かせないのが正規表現です。
その中でも「特定の文字が何回繰り返されるか」を制御する量指定子の扱いは、もっとも頻繁に利用されるテクニックの一つと言えるでしょう。
例えば、電話番号の桁数チェック、郵便番号の形式確認、あるいは特定のキーワードが連続する箇所の抽出など、実務における用途は多岐にわたります。
本記事では、C#のSystem.Text.RegularExpressions名前空間を利用して、正規表現で繰り返し回数を指定するさまざまな方法を、基礎から最新の最適化手法まで詳しく解説します。

正規表現における「量指定子」の基本
正規表現で繰り返しの回数を指定する記号のことを量指定子 (Quantifiers)と呼びます。
量指定子は、その直前にある文字やグループが「何回出現するか」を定義する役割を持ちます。
C#で正規表現を扱う場合、基本的には標準的な正規表現の構文に従いますが、量指定子の動作を正確に理解しておくことは、意図しないマッチングを防ぐために非常に重要です。
まずは、もっとも頻繁に使用される基本的な3つの記号について見ていきましょう。
基本的な量指定子の種類
以下の表は、出現回数を抽象的に指定する際によく使われる記号をまとめたものです。
| 記号 | 意味 | 詳細 |
|---|---|---|
* | 0回以上 | 直前の要素が全く存在しないか、あるいは1回以上繰り返される場合にマッチします。 |
+ | 1回以上 | 直前の要素が少なくとも1回は存在する必要があり、それ以上の繰り返しにもマッチします。 |
? | 0回または1回 | 直前の要素が存在しないか、あるいは1回だけ存在する場合にマッチします(オプション項目)。 |
これらの記号は非常に便利ですが、「回数の上限がない」ことや「最小回数が曖昧」であるため、厳密なバリデーションには向かない場合があります。
そこで、より詳細な回数指定を行うために「波括弧 {}」を使用した記法が用いられます。
数値による具体的な繰り返し回数の指定
特定の回数だけ繰り返したい、あるいは「3回以上5回以下」のように範囲を制限したい場合には、{n,m} という形式の量指定子を使用します。
これにより、数値に基づいた厳密なパターンマッチングが可能になります。

1. 固定回数の指定:{n}
直前の要素がちょうど n 回出現する場合にのみマッチします。
例えば、日本の郵便番号(ハイフンなしの7桁)をチェックしたい場合、数字を意味する \d に対して次のように記述します。
// 数字がちょうど7回繰り返される
string pattern = @"^\d{7}$";
この場合、6桁でも8桁でもマッチせず、必ず7桁である必要があります。
2. 下限のみの指定:{n,}
直前の要素がn 回以上出現する場合にマッチします。
上限はありません。
例えば、「パスワードは8文字以上の英数字でなければならない」といったルールを定義する場合に使用します。
// 英数字が8回以上繰り返される
string pattern = @"^[a-zA-Z0-9]{8,}$";
3. 範囲の指定:{n,m}
直前の要素がn 回以上 m 回以下出現する場合にマッチします。
例えば、ユーザー名の長さを3文字から15文字以内に制限したい場合は、次のように記述します。
// 英単語構成文字が3回から15回繰り返される
string pattern = @"^\w{3,15}$";
このとき、コンマの後にスペースを入れないように注意してください。
正規表現エンジンによっては、スペースを文字として認識してしまい、意図した動作にならないことがあります。
欲張りなマッチと控えめなマッチの使い分け
正規表現の繰り返し指定を扱う上で、必ず理解しておかなければならないのが「欲張りなマッチ (Greedy Match)」と「控えめなマッチ (Lazy/Non-greedy Match)」の違いです。
デフォルトの状態(量指定子のみの状態)では、正規表現エンジンは「できるだけ長く」マッチしようとします。
これを「欲張りなマッチ」と呼びます。
一方、量指定子の直後に ? を付けると、「できるだけ短く」マッチするようになります。

欲張りなマッチの挙動
例えば、HTMLタグのような構造を持つ文字列から中身を取り出したい場合を考えます。
string input = "<a>first <a>second";
string pattern = "<a>.*";
var match = Regex.Match(input, pattern);
Console.WriteLine(match.Value);
// 出力: <a>first <a>second
上記のコードでは、.* が「可能な限り長い一致」を探すため、最初の <a> から最後の までを一つのマッチとして取得してしまいます。
控えめなマッチの挙動
各タグごとにマッチさせたい場合は、量指定子の後に ? を追加して、控えめなマッチを指定します。
string input = "<a>first <a>second";
string pattern = "<a>.*?";
var matches = Regex.Matches(input, pattern);
foreach (Match m in matches)
{
Console.WriteLine(m.Value);
}
// 出力:
// <a>first
// <a>second
このように、*? や +?、{n,m}? を使い分けることで、複雑なドキュメント解析における誤判定を防ぐことができます。
C# での実装:Regexクラスの活用方法
C#で正規表現を利用するには、System.Text.RegularExpressions 名前空間の Regex クラスを使用します。
繰り返し指定を含むパターンを扱う際の、代表的なメソッドと使い方を紹介します。
Regex.IsMatch メソッド
入力文字列がパターンに一致するかどうかを bool 値で返します。
バリデーション(入力チェック)に最適です。
using System.Text.RegularExpressions;
string phoneNumber = "090-1234-5678";
// 数字3桁-数字4桁-数字4桁のパターン
string pattern = @"^\d{3}-\d{4}-\d{4}$";
if (Regex.IsMatch(phoneNumber, pattern))
{
Console.WriteLine("有効な電話番号形式です。");
}
Regex.Matches メソッド
文字列内から、パターンに一致するすべての箇所を抽出します。
抽出結果は MatchCollection として返されます。
string text = "2023/10/01, 2024/01/15, 2025/12/31";
// 4桁/2桁/2桁の形式をすべて抽出
string pattern = @"\d{4}/\d{2}/\d{2}";
MatchCollection matches = Regex.Matches(text, pattern);
foreach (Match match in matches)
{
Console.WriteLine($"見つかった日付: {match.Value}");
}
逐次比較とパフォーマンス
正規表現の繰り返し(特に無限の繰り返し)は、対象の文字列が非常に長い場合にパフォーマンスに影響を与えることがあります。
C#では、正規表現をあらかじめコンパイルして再利用することで、処理を高速化できます。
// インスタンスを生成してオプションを指定
var options = RegexOptions.Compiled | RegexOptions.IgnoreCase;
var regex = new Regex(@"[a-z]{5,}", options);
RegexOptions.Compiled を指定すると、正規表現がMSILコードにコンパイルされ、実行時の速度が向上します。
ただし、起動時のオーバーヘッドが増えるため、頻繁に呼び出されるパターンでのみ使用するのが一般的です。
最新の C# における最適化:GeneratedRegex
.NET 7 以降では、ソースジェネレーターを利用した新しい正規表現の最適化手法 GeneratedRegex が導入されました。
これにより、実行時のコンパイル時間をゼロにし、さらに高いパフォーマンスを実現できます。

GeneratedRegex の実装例
ソースジェネレーターを使用するには、partial クラス内に partial メソッドを定義し、[GeneratedRegex] 属性を付与します。
using System.Text.RegularExpressions;
public partial class Validator
{
// コンパイル時に正規表現エンジンが生成される
[GeneratedRegex(@"^\d{3}-\d{4}$", RegexOptions.IgnoreCase)]
private static partial Regex ZipCodeRegex();
public void Check(string input)
{
if (ZipCodeRegex().IsMatch(input))
{
Console.WriteLine("郵便番号が一致しました。");
}
}
}
この手法は、繰り返し回数の指定が複雑なパターンにおいて、バックトラッキング(後戻り)の最適化を強力にサポートするため、モダンなC#開発では推奨される方法です。
よくある間違いと注意点
正規表現で繰り返しを指定する際、初心者が陥りやすいミスがいくつかあります。
1. エスケープの忘れ
波括弧 {} そのものを文字として検索したい場合は、バックスラッシュ(C#では円記号)でエスケープする必要があります。
- 誤:
{(繰り返し指定の開始とみなされる) - 正:
{(文字としての波括弧にマッチする)
2. 空文字へのマッチ
* (0回以上) や ? (0回または1回) は、「文字が全くなくてもマッチする」という点に注意してください。
意図せず空文字にマッチしてしまい、無限ループのような挙動や予期せぬバリデーション通過を招くことがあります。
少なくとも1文字は必要という場合は、必ず + や {1,} を使用しましょう。
3. バックトラッキングによる「破綻」
量指定子を入れ子(ネスト)にすると、一致しない文字列を評価する際に「指数関数的」に計算量が増大することがあります(これを「カタストロフィック・バックトラッキング」と呼びます)。
- 危険な例:
(a+)+b
このようなパターンに対して非常に長い “aaaaa…” という文字列(最後にbがないもの)を渡すと、プログラムがフリーズしたような状態になることがあります。
C#では、Regex.MatchTimeout を設定することで、こうした問題に対する安全策を講じることが可能です。
// 1秒以内にマッチしなければタイムアウトさせる
TimeSpan timeout = TimeSpan.FromSeconds(1);
Regex.Match(input, pattern, RegexOptions.None, timeout);
まとめ
C#の正規表現において、繰り返し回数の指定は非常に強力かつ柔軟な機能です。
- 量指定子の基本:
*,+,?を使い分け、出現の有無を制御する。 - 数値指定:
{n},{n,},{n,m}を使い、厳密な回数制限を行う。 - マッチの性質:欲張りなマッチと控えめなマッチ(
?)の違いを理解し、抽出範囲を正しく制御する。 - 最適化:大規模な処理では
RegexOptions.Compiledや .NET 7以降のGeneratedRegexを活用する。
これらの知識を組み合わせることで、堅牢でパフォーマンスの高い文字列処理を実装できるようになります。
まずはシンプルな回数指定から試し、徐々に複雑なパターン構築に挑戦してみてください。
正規表現をマスターすることは、C#プログラミングの効率を飛躍的に向上させる一歩となります。
