正規表現では、(, )などのグループ化を行う正規表現要素によって正規表現を部分式に分けてグループ化したり、グループに分かりやすい名前を与えることによって構造化することができます。 System.Text.RegularExpressions名前空間にはRegexクラス以外にも正規表現の処理に関連するクラスがいくつかあり、構造化した正規表現を処理するために用います。

Matchクラスは正規表現にマッチした箇所を表すクラスで、例えばMatchesメソッドを使って正規表現にマッチする箇所を複数取得する場合、マッチした個々の箇所をMatchインスタンスとして参照することができます。 このほかにも、Match.NextMatchメソッドを用いることでマッチする複数の箇所を逐次処理したり、Match.Resultメソッドを用いることで正規表現にマッチした箇所を別の文字列に置き換えて取得するといったこともできます。

Groupクラスは正規表現要素(, )グループ化された箇所を表すクラスで、Match.Groupsプロパティから参照することができます。 例えばPerlやRubyでは、正規表現(\d+)/(\d+)のそれぞれのカッコ内にマッチした文字列は$1, $2といった特殊変数として参照することができますが、.NETでも特殊変数の形ではないもののmatch.Groups[1], match.Groups[2]といったようにMatchクラスを通してグループを参照することができるほか、正規表現による置換に際して置換文字列として使用することができます。 また、"(?<num>\d+)"のような名前付きグループ(名前付きキャプチャ)を使用することもでき、match.Groups["num"]のようにしてマッチした箇所を参照することができます。

さらに、各グループがキャプチャした文字列は、CaptureクラスとしてGroup.Capturesプロパティから参照することができます。 Match・Group・Captureの各クラスは互いに親子関係になっていて、これらのクラスを用いることで正規表現にマッチした箇所の位置(Index)と長さ(Length)、実際にマッチした文字列(Value)を細かく分析することができます。

マッチ箇所の参照および操作(Matchクラス)

Matchクラスは正規表現にマッチした箇所(部分文字列)を表すクラスで、Regex.Matchメソッド・Regex.Matchesメソッドでの戻り値として返されます。 Matchクラスを参照することでマッチした箇所の文字列を参照したり、別の文字列に置換したりすることができます。

Regex.Matchメソッドでは、マッチする箇所がない場合はSuccessプロパティFalseのMatchインスタンスが返されます。

Matchクラスのプロパティを参照して正規表現にマッチした個所の文字列・インデックス・長さを取得する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "The quick brown fox jumps over the lazy dog";
    var pattern = @"\w{6,}"; // 6文字以上の単語

    var m = Regex.Match(text, pattern); // 最初にマッチする箇所を取得

    Console.WriteLine(m.Success);

    // Match.Successプロパティの値は、IsMatchメソッドを呼び出した結果と同じ
    Console.WriteLine(Regex.IsMatch(text, pattern));
  }
}
Matchクラスのプロパティを参照して正規表現にマッチした個所の文字列・インデックス・長さを取得する
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim text As String = "The quick brown fox jumps over the lazy dog"
    Dim pattern As String = "\w{6,}" ' 6文字以上の単語

    Dim m As Match = Regex.Match(text, pattern) ' 最初にマッチする箇所を取得

    Console.WriteLine(m.Success)

    ' Match.Successプロパティの値は、IsMatchメソッドを呼び出した結果と同じ
    Console.WriteLine(Regex.IsMatch(text, pattern))
  End Sub
End Class
実行結果
False
False

上記の例にもあるように、Matchメソッドが返すMatchインスタンスのSuccessプロパティは、Regex.IsMatchメソッドによってパターンマッチングを行った場合の結果と同じになります。


Regex.Matchメソッドで正規表現にマッチする箇所が見つかった場合は、SuccessプロパティがTrueのMatchインスタンスが返されます。 この際、Valueプロパティには実際にマッチした部分の文字列が設定されます。 同様に、IndexプロパティLengthプロパティにはマッチした部分のインデックスと長さが設定されます。

Match.Successプロパティを参照して正規表現にマッチする個所があるかどうか調べる
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "The quick brown fox jumps over the lazy dog";
    var pattern = @"\w{5,}"; // 5文字以上の単語

    var m = Regex.Match(text, pattern); // 最初にマッチする箇所を取得

    if (m.Success) { // マッチする個所があれば、SuccessはTrueとなる
      Console.WriteLine(m.Value); // マッチした個所の文字列を取得する

      // Index・Lengthプロパティにはマッチした個所のインデックスと長さが設定される
      // (実際にマッチした個所の文字列はValueプロパティで取得できるが、
      // 次のようにしても取得することができる)
      Console.WriteLine(text.Substring(m.Index, m.Length));
    }
  }
}
Match.Successプロパティを参照して正規表現にマッチする個所があるかどうか調べる
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim text As String = "The quick brown fox jumps over the lazy dog"
    Dim pattern As String = "\w{5,}" ' 5文字以上の単語

    Dim m As Match = Regex.Match(text, pattern) ' 最初にマッチする箇所を取得

    If m.Success Then ' マッチする個所があれば、SuccessはTrueとなる
      Console.WriteLine(m.Value) ' マッチした個所の文字列を取得する

      ' Index・Lengthプロパティにはマッチした個所のインデックスと長さが設定される
      ' (実際にマッチした個所の文字列はValueプロパティで取得できるが、
      ' 次のようにしても取得することができる)
      Console.WriteLine(text.Substring(m.Index, m.Length))
    End If
  End Sub
End Class
実行結果
quick
quick

MatchCollectionクラス

MatchCollectionクラスは、名前の通り複数のMatchを格納するためのコレクションクラスです。 Regex.Matchesメソッドは戻り値の型がMatchCollectionとなっていて、正規表現にマッチした箇所すべてに対応するMatchが格納された状態で返されます。

Regex.Matchesメソッドから返されるMatchCollectionには、正規表現にマッチした箇所のみを表すMatchが含まれるため、このMatchCollectionからはすべてSuccessプロパティがTrueのMatchが列挙されることになります。 そのため、個別にSuccessプロパティをチェックする必要はありません。

Regex.Matchesメソッドで正規表現にマッチする個所すべてに対応するMatchインスタンスを取得する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "The quick brown fox jumps over the lazy dog";
    var pattern = @"\w{4,}"; // 4文字以上の単語

    // マッチする個所すべてを列挙
    // (MatchCollectionは非ジェネリックで型指定されていないIEnumeratorを返すため、varによる暗黙的な型指定はできない)
    foreach (Match m in Regex.Matches(text, pattern)) {
      Console.WriteLine(m.Value);
    }
  }
}
Regex.Matchesメソッドで正規表現にマッチする個所すべてに対応するMatchインスタンスを取得する
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim text As String = "The quick brown fox jumps over the lazy dog"
    Dim pattern As String = "\w{4,}" ' 4文字以上の単語

    ' マッチする個所すべてを列挙
    For Each m As Match In Regex.Matches(text, pattern)
      Console.WriteLine(m.Value)
    Next
  End Sub
End Class
実行結果
quick
brown
jumps
over
lazy

MatchCollectionは当初、非ジェネリックなIEnumerableのみを実装した型指定されていないコレクションとして登場し、その後.NET Standard 2.1/.NET Core 2.0にてIEnumerable<Match>/ICollection<Match>/IList<Match>を実装するようになり、型指定されたコレクションとして扱うことができるようになっています。

一方、IEnumerable<Match>の実装以降でも、MatchCollection.GetEnumeratorメソッドは(おそらく)互換性のため型指定されていないIEnumeratorを返す実装のままとなっています。 このため、MatchCollectionをforeachで列挙する場合、varによる暗黙的な型指定を行うとobjectとして列挙されます

代替手法やMatchCollectionのジェネリックインターフェイスについてより詳しくは§.MatchCollection・GroupCollection・CaptureCollectionとジェネリックコレクションインターフェイスを参照してください。

MatchCollectionは当初IEnumerableのみを実装した型指定されていないコレクションとして登場し、その後.NET Standard 2.1/.NET Core 2.0にてIEnumerable<Match>/ICollection<Match>/IList<Match>を実装するようになり、型指定されたコレクションとして扱うことができるようになっています。

一方、IEnumerable<Match>の実装以降でも、MatchCollection.GetEnumeratorメソッドは(おそらく)互換性のため型指定されていないIEnumeratorを返す実装のままとなっています。 このため、MatchCollectionをforeachで列挙する場合、varによる暗黙的な型指定を行うとobjectとして列挙されます


Regex.Matchesメソッドでは、正規表現にマッチする箇所がなければ空のMatchCollectionを返します。

Regex.Matchesメソッドで正規表現にマッチする個所の数を表示する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "The quick brown fox jumps over the lazy dog";
    var pattern = @"\w{10,}"; // 10文字以上の単語

    // マッチする個所すべてを取得
    var matches = Regex.Matches(text, pattern);

    // マッチした数を取得・表示する
    Console.WriteLine(matches.Count);
  }
}
Regex.Matchesメソッドで正規表現にマッチする個所の数を表示する
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim text As String = "The quick brown fox jumps over the lazy dog"
    Dim pattern As String = "\w{10,}" ' 10文字以上の単語

    ' マッチする個所すべてを取得
    Dim matches As MatchCollection = Regex.Matches(text, pattern)

    ' マッチした数を取得・表示する
    Console.WriteLine(matches.Count)
  End Sub
End Class
実行結果
0

Match.NextMatchメソッド

Match.NextMatchメソッドは、同じ正規表現が次にマッチする箇所を返すメソッドです。 Regex.Matchメソッドは最初にマッチした箇所に対応するMatchインスタンスを返しますが、NextMatchメソッドを呼び出すとその次にマッチする箇所に対応するMatchインスタンスを返します。

Match.NextMatchメソッドを使って同じ正規表現が次にマッチする箇所を取得する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "The quick brown fox jumps over the lazy dog";
    var pattern = @"\w{5,}"; // 5文字以上の単語

    // 最初に一致する箇所を取得
    var m1 = Regex.Match(text, pattern);

    Console.WriteLine(m1.Value);

    // 同じ正規表現が次にマッチする箇所を取得
    var m2 = m1.NextMatch();

    Console.WriteLine(m2.Value);
  }
}
Match.NextMatchメソッドを使って同じ正規表現が次にマッチする箇所を取得する
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim text As String = "The quick brown fox jumps over the lazy dog"
    Dim pattern As String = "\w{5,}" ' 5文字以上の単語

    ' 最初に一致する箇所を取得
    Dim m1 As Match = Regex.Match(text, pattern)

    Console.WriteLine(m1.Value)

    ' 同じ正規表現が次にマッチする箇所を取得
    Dim m2 As Match = m1.NextMatch()

    Console.WriteLine(m2.Value)
  End Sub
End Class
実行結果
quick
brown

次にマッチする箇所がない場合、NextMatchメソッドはSuccessプロパティがFalseのMatchインスタンスを返します。


Regex.Matchesメソッドではマッチした箇所すべてが同時に返されますが、例えば、Regex.Matchメソッドで文字列中の最初にマッチする箇所を取得したのち、一旦何らかの処理を行ってから次のマッチ箇所に移動して処理を継続したいといった場合には、NextMatchメソッドを使うことができます。

マッチ箇所が多数になると想定される場合、Regex.Matchesメソッドではそのすべての探索を終えるまで結果が返されませんが、NextMatchメソッドを使うと、まずRegex.Matchメソッドで最初にマッチする箇所を探索し、その後NextMatchメソッドでマッチする箇所を逐次処理していく、といったことができます。

次の例では、正規表現にマッチする全ての箇所を、Regex.Match+Match.NextMatchメソッドと、Regex.Matchesメソッドを使って取得した場合の違いを示しています。 結果はどちらも同じになりますが、マッチ箇所を逐次取得するか、一度に取得するかの違いがある点に注目してください。

Regex.Match+Match.NextMatchメソッドを使ってマッチ箇所を逐次処理する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "The quick brown fox jumps over the lazy dog";
    var pattern = @"\w{4,}"; // 4文字以上の単語

    var m = Regex.Match(text, pattern); // 最初にマッチする箇所を取得

    while (m.Success) {
      // マッチした箇所の文字列を表示する
      Console.WriteLine(m.Value);

      // 次にマッチする箇所を取得する
      m = m.NextMatch();
    }
  }
}
Regex.Match+Match.NextMatchメソッドを使ってマッチ箇所を逐次処理する
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim text As String = "The quick brown fox jumps over the lazy dog"
    Dim pattern As String = "\w{4,}" ' 5文字以上の単語

    Dim m As Match = Regex.Match(text, pattern) ' 最初にマッチする箇所を取得

    Do While m.Success
      ' マッチした箇所の文字列を表示する
      Console.WriteLine(m.Value)

      ' 次にマッチする箇所を取得する
      m = m.NextMatch()
    Loop
  End Sub
End Class
実行結果
quick
brown
jumps
over
lazy
Regex.Matchesメソッドを使ってマッチ箇所を一括して取得してから処理する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "The quick brown fox jumps over the lazy dog";
    var pattern = @"\w{4,}"; // 4文字以上の単語

    // マッチする箇所すべてを取得して列挙
    foreach (Match m in Regex.Matches(text, pattern)) {
      // マッチした箇所の文字列を表示する
      Console.WriteLine(m.Value);
    }
  }
}
Regex.Matchesメソッドを使ってマッチ箇所を一括して取得してから処理する
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim text As String = "The quick brown fox jumps over the lazy dog"
    Dim pattern As String = "\w{4,}" ' 5文字以上の単語

    ' マッチする箇所すべてを取得して列挙
    For Each m As Match In Regex.Matches(text, pattern)
      ' マッチした箇所の文字列を表示する
      Console.WriteLine(m.Value)
    Next
  End Sub
End Class
実行結果
quick
brown
jumps
over
lazy

Match.Resultメソッド

Match.Resultメソッドは、マッチした箇所を引数で指定した文字列に置き換えます。 このメソッドは置換文字列によってマッチ箇所を別の文字列に置き換えた結果を取得するために使うことができます。

Regex.Replaceメソッドで置換文字列を指定した場合、元の文字列全体に同一の置換文字列を適用した結果が返されます。 一方Match.Resultメソッドでは、マッチした箇所ごとに置換文字列を指定して置換した結果を取得することができます。

次の例では、文字列中にある mm/dd/yyyy 形式の日付部分を探索し、Match.Resultメソッドを使って yyyy-mm-dd の形式に置換して表示しています。 正規表現(?<y>...)でグループ化した部分は置換文字列${y}によって取得することができるため、これを使って日付の年月日の順序を変えています。 また、比較としてRegex.Replaceメソッドを使って置換した場合も併記しています。

Match.Resultメソッドを使ってmm/dd/yyyy形式の日付部分をyyyy-mm-dd形式に置換する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "02/29/2016 00:00:00";

    // mm/dd/yyyy形式の日付 (月日年の正規表現をそれぞれグループ名m, d, yでグループ化)
    var pattern = @"(?<m>\d{2})/(?<d>\d{2})/(?<y>\d{4})";

    var m = Regex.Match(text, pattern);

    // マッチした部分をyyyy-mm-ddの形式に置換した結果を表示
    // (グループm, d, yにマッチした箇所をy-m-dの順で表示)
    Console.WriteLine(m.Result("${y}-${m}-${d}"));

    // 上記と同じ結果を生成するコード
    Console.WriteLine(m.Groups["y"].Value + "-" + m.Groups["m"].Value + "-" + m.Groups["d"].Value);

    // (Regex.Replaceメソッドでは、入力文字列全体に対して置換が行われる)
    Console.WriteLine(Regex.Replace(text, pattern, "${y}-${m}-${d}"));
  }
}
Match.Resultメソッドを使ってmm/dd/yyyy形式の日付部分をyyyy-mm-dd形式に置換する
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim text As String = "02/29/2016 00:00:00"

    ' mm/dd/yyyy形式の日付 (月日年の正規表現をそれぞれグループ名m, d, yでグループ化)
    Dim pattern As String = "(?<m>\d{2})/(?<d>\d{2})/(?<y>\d{4})"

    Dim m As Match = Regex.Match(text, pattern)

    ' マッチした部分をyyyy-mm-ddの形式に置換した結果を表示
    ' (グループm, d, yにマッチした箇所をy-m-dの順で表示)
    Console.WriteLine(m.Result("${y}-${m}-${d}"))

    ' 上記と同じ結果を生成するコード
    Console.WriteLine(m.Groups("y").Value + "-" + m.Groups("m").Value + "-" + m.Groups("d").Value)

    ' (Regex.Replaceメソッドでは、入力文字列全体に対して置換が行われる)
    Console.WriteLine(Regex.Replace(text, pattern, "${y}-${m}-${d}"))
  End Sub
End Class
実行結果
2016-02-29
2016-02-29
2016-02-29 00:00:00

グループ化およびグループ化した部分の置換については後述の§.グループ化とグループ番号および§.グループと置換文字列も合わせて参照してください。

マッチする箇所がない状態(SuccessプロパティがFalse)のMatchインスタンスに対してResultメソッドを呼び出した場合、例外NotSupportedExceptionがスローされます。

置換文字列を使った置換については正規表現によるパターンマッチングと文字列操作 §.マッチした文字列への置換 (置換の正規表現要素)、置換文字列として使用できる置換の正規表現要素については.NET Frameworkで使用できる正規表現 §.置換を参照してください。


Match.Resultメソッドを使ったもうひとつ別の例として、次の例では正規表現にマッチする箇所をすべて取得し、マッチした箇所を強調表示しています。 この例で用いている置換文字列の$`および$'は、それぞれマッチした箇所の前と後にある文字列を参照する正規表現要素、$0は実際にマッチした文字列を参照する正規表現要素です。

Match.Resultメソッドを使って正規表現にマッチする箇所を強調表示する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "The quick brown fox jumps over the lazy dog";
    var pattern = @"\w{4,}"; // 4文字以上の単語

    Console.WriteLine(text);
    Console.WriteLine();

    foreach (Match m in Regex.Matches(text, pattern)) {
      Console.WriteLine(m.Result("$`<$0>$'"));

      // 上記と同じ結果を生成するコード
      //Console.WriteLine(text.Substring(0, m.Index) + "<" + m.Value + ">" + text.Substring(m.Index + m.Length));
    }
  }
}
Match.Resultメソッドを使って正規表現にマッチする箇所を強調表示する
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim text As String = "The quick brown fox jumps over the lazy dog"
    Dim pattern As String = "\w{4,}" ' 4文字以上の単語

    Console.WriteLine(text)
    Console.WriteLine()

    For Each m As Match In Regex.Matches(text, pattern)
      Console.WriteLine(m.Result("$`<$0>$'"))

      ' 上記と同じ結果を生成するコード
      'Console.WriteLine(text.Substring(0, m.Index) + "<" + m.Value + ">" + text.Substring(m.Index + m.Length))
    Next
  End Sub
End Class
実行結果
The quick brown fox jumps over the lazy dog

The <quick> brown fox jumps over the lazy dog
The quick <brown> fox jumps over the lazy dog
The quick brown fox <jumps> over the lazy dog
The quick brown fox jumps <over> the lazy dog
The quick brown fox jumps over the <lazy> dog

グループ(Groupクラス)

Groupクラスは正規表現要素()グループ化された正規表現にマッチした部分を表すクラスです。 Matchクラスが正規表現全体にマッチした箇所を表すのに対し、Groupクラスは正規表現のうちグループ化された部分にマッチした箇所を表します。 Matchクラスとは異なり、GroupクラスはRegexクラスから直接返されることはなく、Match.Groupsプロパティを通じて参照します。

グループ化とグループ番号

正規表現要素()を用いると、正規表現をグループ化することができます。 例えば、yyyy-dd-mm形式の日付にマッチする正規表現は\d{4}-\d{2}-\d{2}と記述することができますが、この正規表現の年月日部分をそれぞれグループ化すると(\d{4})-(\d{2})-(\d{2})となります。

このようにしてグループ化した正規表現では、各グループにマッチした箇所を個別に参照することができます。 また、各グループには先頭側にあるグループから順に1から始まるインデックス(グループ番号)が割り当てられます。

グループを含む正規表現では、正規表現全体にマッチした文字列はMatchクラスによって参照することができると同時に、それぞれのグループにマッチした文字列はMatch.Groupsプロパティを通してmatch.Group[1], match.Group[2]... のようにGroupクラスの形で個別に参照することができます。

Match.Groupsプロパティを使ってyyyy-mm-dd形式にマッチする文字列の年月日をそれぞれ取得する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "2016-02-29";
    var pattern = @"(\d{4})-(\d{2})-(\d{2})"; // yyyy-mm-dd形式の日付

    var m = Regex.Match(text, pattern);

    // マッチした文字列のうち、yyyy, mm, ddのそれぞれのグループに対応する箇所を取得する
    Console.WriteLine(m.Groups[1].Value);
    Console.WriteLine(m.Groups[2].Value);
    Console.WriteLine(m.Groups[3].Value);
  }
}
Match.Groupsプロパティを使ってyyyy-mm-dd形式にマッチする文字列の年月日をそれぞれ取得する
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim text As String = "2016-02-29"
    Dim pattern As String = "(\d{4})-(\d{2})-(\d{2})" ' yyyy-mm-dd形式の日付

    Dim m As Match = Regex.Match(text, pattern)

    ' マッチした文字列のうち、yyyy, mm, ddのそれぞれのグループに対応する箇所を取得する
    Console.WriteLine(m.Groups(1).Value)
    Console.WriteLine(m.Groups(2).Value)
    Console.WriteLine(m.Groups(3).Value)
  End Sub
End Class
実行結果
2016
02
29

また、Matchクラスと同様に、Group.Valueプロパティでグループ化した部分にマッチした文字列が参照できるほか、IndexプロパティLengthプロパティでその位置と長さを取得することができます。

グループ内にグループを記述する、つまりグループを入れ子にすることもできます。 グループが入れ子になっている場合、最も外側・先頭側にあるグループから順にグループ番号が割り当てられます。 グループ番号の割れ当て方と、入れ子になったグループの場合については.NET Frameworkで使用できる正規表現 §.グループ番号の割り振られ方を参照してください。

マッチ箇所全体を表すグループ ($0)

インデックスが0のグループは特殊なグループで、正規表現全体にマッチした部分を表します。 つまり、match.Group[0]matchそのものと同じ値となります。 これはPerlなどの正規表現における変数$0に相当するものです。

グループ番号0を参照して正規表現全体にマッチした箇所を取得する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "2016-02-29 00:00:00";
    var pattern = @"\d{4}-\d{2}-\d{2}"; // yyyy-mm-dd形式の日付

    var m = Regex.Match(text, pattern);

    // グループ番号0は正規表現にマッチした文字列全体を表す(Match.Valueプロパティと同じ値となる)
    Console.WriteLine(m.Groups[0].Value);
    Console.WriteLine(m.Value);
  }
}
グループ番号0を参照して正規表現全体にマッチした箇所を取得する
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim text As String = "2016-02-29 00:00:00"
    Dim pattern As String = "\d{4}-\d{2}-\d{2}" ' yyyy-mm-dd形式の日付

    Dim m As Match = Regex.Match(text, pattern)

    ' グループ番号0は正規表現にマッチした文字列全体を表す(Match.Valueプロパティと同じ値となる)
    Console.WriteLine(m.Groups(0).Value)
    Console.WriteLine(m.Value)
  End Sub
End Class
実行結果
2016-02-29
2016-02-29

グループ化された正規表現を含まない場合でも、常にインデックス0のグループを参照することができます。 正規表現にマッチする箇所がない場合(SuccessプロパティがFalseのMatch)でも同様にインデックス0のグループを参照することができ、このときグループの値は空の文字列となります。

GroupCollectionクラス

GroupCollectionクラスは、名前の通り複数のGroupを格納するためのコレクションクラスです。 Match.Groupsプロパティの型はGroupCollectionとなっていて、マッチした正規表現のグループすべてに対応するGroupが格納された状態で返されます。

Match.Groupsプロパティはインデクサによってグループ番号・グループ名に対応するGroupを取得することができるほか、foreach/For Eachによってすべてのグループを列挙することもできます。 このとき、正規表現内にグループが含まれているかどうかに関わらず、常にマッチ箇所全体を表すグループが列挙されます。

Match.Groupsプロパティからすべてのグループを列挙する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "2016-02-29 00:00:00";
    var pattern = @"(\d{4})-(\d{2})-(\d{2})"; // yyyy-mm-dd形式の日付

    var m = Regex.Match(text, pattern);

    if (m.Success) {
      // Match.Groupsプロパティから、すべてのグループを取得して表示する
      // (GroupCollectionは非ジェネリックで型指定されていないIEnumeratorを返すため、varによる暗黙的な型指定はできない)
      foreach (Group g in m.Groups) {
        Console.WriteLine(g.Value);
      }
    }
  }
}
Match.Groupsプロパティからすべてのグループを列挙する
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim text As String = "2016-02-29 00:00:00"
    Dim pattern As String = "(\d{4})-(\d{2})-(\d{2})" ' yyyy-mm-dd形式の日付

    Dim m As Match = Regex.Match(text, pattern)

    If m.Success Then
      ' Match.Groupsプロパティから、すべてのグループを取得して表示する
      For Each g As Group In m.Groups
        Console.WriteLine(g.Value)
      Next
    End If
  End Sub
End Class
実行結果
2016-02-29
2016
02
29

GroupCollectionは当初、非ジェネリックなIEnumerableのみを実装した型指定されていないコレクションとして登場し、その後.NET Standard 2.1/.NET Core 2.0にてIEnumerable<Group>/ICollection<Group>/IList<Group>を実装するようになり、型指定されたコレクションとして扱うことができるようになっています。

一方、IEnumerable<Group>の実装以降でも、GroupCollection.GetEnumeratorメソッドは(おそらく)互換性のため型指定されていないIEnumeratorを返す実装のままとなっています。 このため、GroupCollectionをforeachで列挙する場合、varによる暗黙的な型指定を行うとobjectとして列挙されます

代替手法やGroupCollectionのジェネリックインターフェイスについてより詳しくは§.MatchCollection・GroupCollection・CaptureCollectionとジェネリックコレクションインターフェイスを参照してください。

.NET Core 3.0以降では、GroupCollectionはIReadOnlyDictionary<string, Group>インターフェイスを実装しています。 これにより、GroupCollectionをDictionaryと同様に扱うことができるようになっています。 詳しくは§.GroupCollectionとIReadOnlyDictionaryで解説しています。

名前付きグループ

正規表現要素(?<name>)を使うと、正規表現のグループ化と同時に名前を与えることができます。 グループに名前を与えることにより、正規表現の構造を把握しやすくすることができ、またグループを参照するコードの可読性を向上させることができます。 例えば正規表現\d{4}にグループ名nameを与える場合は(?<name>\d{4})と記述します。

Match.Groupsプロパティから名前付きグループを参照する場合は、match.Groups["name"]のように、インデックスの代わりにグループ名を文字列で指定します。

グループ名が与えられている場合でも、インデックスを使って個々のグループを参照することもできます。 各グループは、名前付きかどうかによらず、常に同じ順序でグループ番号が割り当てられます。 (.NET Frameworkで使用できる正規表現 §.グループ番号の割り振られ方)

Match.Groupsプロパティと名前付きグループを使ってyyyy-mm-dd形式にマッチする文字列の年月日をそれぞれ取得する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "2016-02-29";
    var pattern = @"(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})"; // yyyy-mm-dd形式の日付
    //var pattern = @"(\d{4})-(\d{2})-(\d{2})"; // 上記と同等の正規表現をグループ名なしで記述した場合

    var m = Regex.Match(text, pattern);

    // マッチした文字列のうち、グループ名'y', 'm', 'd'のそれぞれに対応する箇所を取得する
    Console.WriteLine(m.Groups["y"].Value);
    Console.WriteLine(m.Groups["m"].Value);
    Console.WriteLine(m.Groups["d"].Value);
    Console.WriteLine();

    // グループ名を与えている場合でも、各グループをインデックス(グループ番号)で参照することもできる
    Console.WriteLine(m.Groups[1].Value);
    Console.WriteLine(m.Groups[2].Value);
    Console.WriteLine(m.Groups[3].Value);
  }
}
Match.Groupsプロパティと名前付きグループを使ってyyyy-mm-dd形式にマッチする文字列の年月日をそれぞれ取得する
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim text As String = "2016-02-29"
    Dim pattern As String = "(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})" ' yyyy-mm-dd形式の日付
    'Dim pattern As String = "(\d{4})-(\d{2})-(\d{2})" ' 上記と同等の正規表現をグループ名なしで記述した場合

    Dim m As Match = Regex.Match(text, pattern)

    ' マッチした文字列のうち、グループ名'y', 'm', 'd'のそれぞれに対応する箇所を取得する
    Console.WriteLine(m.Groups("y").Value)
    Console.WriteLine(m.Groups("m").Value)
    Console.WriteLine(m.Groups("d").Value)
    Console.WriteLine()

    ' グループ名を与えている場合でも、各グループをインデックス(グループ番号)で参照することもできる
    Console.WriteLine(m.Groups(1).Value)
    Console.WriteLine(m.Groups(2).Value)
    Console.WriteLine(m.Groups(3).Value)
  End Sub
End Class
実行結果
2016
02
29

2016
02
29

正規表現の異なる部分に同じグループ名を与えることもできます。 この場合、それぞれに部分にマッチした箇所は同じグループ名を使って参照することができます。 これにより、正規表現中で同じ意味を持つ部分を同一の名前でグループ化することができ、それらのいずれかにマッチした箇所を単一のグループ名で抽出することができます。

正規表現の異なる部分を同じグループ名で参照・抽出する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "02/29/2012 2016-02-29";

    // mm/dd/yyyy形式またはyyyy-mm-dd形式の日付
    // (それぞれの年部分をグループ名"y"でグループ化)
    var pattern = @"\d{2}/\d{2}/(?<y>\d{4})|(?<y>\d{4})-\d{2}-\d{2}";

    foreach (Match m in Regex.Matches(text, pattern)) {
      // グループ名"y"にマッチした部分を表示
      Console.WriteLine(m.Groups["y"].Value);
    }
  }
}
正規表現の異なる部分を同じグループ名で参照・抽出する
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim text As String = "02/29/2012 2016-02-29"

    ' mm/dd/yyyy形式またはyyyy-mm-dd形式の日付
    ' (それぞれの年部分をグループ名"y"でグループ化)
    Dim pattern As String = "\d{2}/\d{2}/(?<y>\d{4})|(?<y>\d{4})-\d{2}-\d{2}"

    For Each m As Match In Regex.Matches(text, pattern)
      ' グループ名"y"にマッチした部分を表示
      Console.WriteLine(m.Groups("y").Value)
    Next
  End Sub
End Class
実行結果
2012
2016

グループと置換文字列

Regex.ReplaceメソッドMatch.Resultメソッドでは、置換文字列として$nを指定すると、グループにマッチした文字列に置換することができます。 例えば$1とすればグループ番号1のグループにマッチした文字列に置換されます。 以降同様に、$2$3…と参照することができます。 $0とした場合は、マッチした文字列全体が参照されます。

グループ番号の置換文字列を使ってmm/dd/yyyy形式の日付をyyyy-mm-dd形式に置換する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "02/29/2016 00:00:00";

    Console.WriteLine(text);

    // mm/dd/yyyy形式の日付 (グループ番号1=月、番号2=日、番号3=年となる)
    var pattern = @"(\d{2})/(\d{2})/(\d{4})";

    // mm/dd/yyyy形式の日付をyyyy-mm-ddの形式に置換して表示
    Console.WriteLine(Regex.Replace(text, pattern, @"$3-$1-$2"));
  }
}
グループ番号の置換文字列を使ってmm/dd/yyyy形式の日付をyyyy-mm-dd形式に置換する
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim text As String = "02/29/2016 00:00:00"

    Console.WriteLine(text)

    ' mm/dd/yyyy形式の日付 (グループ番号1=月、番号2=日、番号3=年となる)
    Dim pattern As String = "(\d{2})/(\d{2})/(\d{4})"

    ' mm/dd/yyyy形式の日付をyyyy-mm-ddの形式に置換して表示
    Console.WriteLine(Regex.Replace(text, pattern, "$3-$1-$2"))
  End Sub
End Class
実行結果
02/29/2016 00:00:00
2016-02-29 00:00:00

PerlやRubyの正規表現では$nを変数として用いることができますが、C#およびVB.NETでは$nを変数として用いることはできません。


グループ番号ではなく名前付きグループを参照する場合は${name}とします。 先の例を名前付きグループを使ったものに書き換えると次のようになります。

名前付きグループの置換文字列を使ってmm/dd/yyyy形式の日付をyyyy-mm-dd形式に置換する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "02/29/2016 00:00:00";

    Console.WriteLine(text);

    // mm/dd/yyyy形式の日付 (月日年の正規表現をそれぞれグループ名m, d, yでグループ化)
    var pattern = @"(?<m>\d{2})/(?<d>\d{2})/(?<y>\d{4})";

    // mm/dd/yyyy形式の日付をyyyy-mm-ddの形式に置換して表示
    Console.WriteLine(Regex.Replace(text, pattern, @"${y}-${m}-${d}"));
  }
}
名前付きグループの置換文字列を使ってmm/dd/yyyy形式の日付をyyyy-mm-dd形式に置換する
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim text As String = "02/29/2016 00:00:00"

    Console.WriteLine(text)

    ' mm/dd/yyyy形式の日付 (月日年の正規表現をそれぞれグループ名m, d, yでグループ化)
    Dim pattern As String = "(?<m>\d{2})/(?<d>\d{2})/(?<y>\d{4})"

    ' mm/dd/yyyy形式の日付をyyyy-mm-ddの形式に置換して表示
    Console.WriteLine(Regex.Replace(text, pattern, "${y}-${m}-${d}"))
  End Sub
End Class
実行結果
02/29/2016 00:00:00
2016-02-29 00:00:00

$n${name}の他にも置換文字列にはいくつか種類があります。 詳しくは.NET Frameworkで使用できる正規表現 §.置換を参照してください。

グループ化の除外 (非キャプチャグループ)

()および(?<name>)ではグループ番号やグループ名を与えたグループ化がなされますが、正規表現要素(?:)を使うと、グループ番号やグループ名を持たないグループ(キャプチャされないグループ・非キャプチャグループ)を構成することができます。 非キャプチャグループは、例えば正規表現を記述する上で()を使いたいが、Match.Groupsプロパティ等で処理する必要のない・除外したいグループを構成したいような場合に用いることができます。

次の例では、yyyy-mm-dd形式またはyyyy/mm/dd形式の日付にマッチする文字列を探索し、yyyy年mm月dd日の形式に置換しています。 この際、区切り文字の正規表現(/|-)を非キャプチャグループとすることでグループ化から除外しています。 これにより、年・月・日のそれぞれ対応するグループのみにグループ番号1・2・3が割り当てられます。

非キャプチャグループを含む正規表現を使ってyyyy-mm-ddまたはyyyy/mm/dd形式の日付をyyyy年mm月dd日の形式に置換する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "2016/01/23 2016-04-05";

    // yyyy-mm-dd形式またはyyyy/mm/dd形式の日付
    // (年月日部分のみがグループ化され、区切り文字の部分はグループ化から除外されるため
    //  グループ番号1=年、番号2=月、番号3=日となる)
    var pattern = @"(\d{4})(?:/|-)(\d{2})(?:/|-)(\d{2})";

    // 上記の正規表現にマッチする日付をyyyy年mm月dd日の形式に置換する
    Console.WriteLine(Regex.Replace(text, pattern, "$1年$2月$3日"));
  }
}
非キャプチャグループを含む正規表現を使ってyyyy-mm-ddまたはyyyy/mm/dd形式の日付をyyyy年mm月dd日の形式に置換する
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim text As String = "2016/01/23 2016-04-05"

    ' yyyy-mm-dd形式またはyyyy/mm/dd形式の日付
    ' (年月日部分のみがグループ化され、区切り文字の部分はグループ化から除外されるため
    '  グループ番号1=年、番号2=月、番号3=日となる)
    Dim pattern As String = "(\d{4})(?:/|-)(\d{2})(?:/|-)(\d{2})"

    ' 上記の正規表現にマッチする日付をyyyy年mm月dd日の形式に置換する
    Console.WriteLine(Regex.Replace(text, pattern, "$1年$2月$3日"))
  End Sub
End Class
実行結果
2016年01月23日 2016年04月05日

明示的なグループ化 (RegexOptions.ExplicitCapture)

RegexOptions.ExplicitCaptureを指定した場合、明示的に名前を与えたグループのみキャプチャされるようになり、それ以外のグループ化構成要素はグループ化から除外されます。 正規表現要素(?:)が明示的にキャプチャの除外対象を指定するものであるのに対し、RegexOptions.ExplicitCaptureは名前付きグループと組み合わせて明示的にキャプチャの対象を指定するものです。

次の例では、RegexOptionsがNoneの場合(指定しない場合のデフォルト)と、RegexOptions.ExplicitCaptureの場合でキャプチャされる結果の違いを示しています。 RegexOptions.Noneではグループ名を与えていないグループもキャプチャされるのに対し、RegexOptions.ExplicitCaptureではグループ名を与えているグループのみがキャプチャされています。

RegexOptions.ExplicitCaptureを指定して明示的にグループ名を指定したグループのみをキャプチャする
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "2016/02/29 00:00:00";

    // yyyy/mm/dd形式の日付 (yyyy部分のみ名前付きグループでグループ化)
    var pattern = @"(?<y>\d{4})/(\d{2})/(\d{2})";

    // RegexOptions.Noneの場合にキャプチャされるグループを列挙して表示
    Console.WriteLine("[RegexOptions.None]");

    foreach (Group g in Regex.Match(text, pattern, RegexOptions.None).Groups) {
      Console.WriteLine(g.Value);
    }

    // RegexOptions.ExplicitCaptureの場合にキャプチャされるグループを列挙して表示
    Console.WriteLine("[RegexOptions.ExplicitCapture]");

    foreach (Group g in Regex.Match(text, pattern, RegexOptions.ExplicitCapture).Groups) {
      Console.WriteLine(g.Value);
    }
  }
}
RegexOptions.ExplicitCaptureを指定して明示的にグループ名を指定したグループのみをキャプチャする
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim text As String = "2016/02/29 00:00:00"

    Console.WriteLine(text)

    ' yyyy/mm/dd形式の日付 (yyyy部分のみ名前付きグループでグループ化)
    Dim pattern As String = "(?<y>\d{4})/(\d{2})/(\d{2})"

    ' RegexOptions.Noneの場合にキャプチャされるグループを列挙して表示
    Console.WriteLine("[RegexOptions.None]")

    For Each g As Group In Regex.Match(text, pattern, RegexOptions.None).Groups
      Console.WriteLine(g.Value)
    Next

    ' RegexOptions.ExplicitCaptureの場合にキャプチャされるグループを列挙して表示
    Console.WriteLine("[RegexOptions.ExplicitCapture]")

    For Each g As Group In Regex.Match(text, pattern, RegexOptions.ExplicitCapture).Groups
      Console.WriteLine(g.Value)
    Next
  End Sub
End Class
実行結果
[RegexOptions.None]
2016/02/29
02
29
2016
[RegexOptions.ExplicitCapture]
2016/02/29
2016

上記の結果にもあるように、RegexOptions.ExplicitCaptureによって明示的なグループ化を行った場合でも、match.Groupsプロパティにはインデックスが0のグループ、つまりマッチした箇所全体を表すグループが常に含まれます。

Regex.Splitメソッドでグループ化した正規表現を区切りとして分割する場合、RegexOptions.ExplicitCaptureを指定するかどうかで結果が変わります。 具体的な結果の違いについては§.グループ化された正規表現による分割 (Regex.Split)を参照してください。

グループ化された正規表現による分割 (Regex.Split)

Regex.Splitメソッドを用いた正規表現による文字列分割では、区切りとして指定する正規表現がキャプチャされるかどうかによって動作が異なります。 つまり、グループ化されているかどうか、またグループ化されている場合は非キャプチャグループかどうかで結果が変わります。 区切りの正規表現がグループ化されている場合、分割した結果にはグループにマッチした部分、つまり区切り文字列自体も含まれます。 一方、グループ化されていない場合・非キャプチャグループの場合は、分割した結果に区切り文字列は含まれません

次の例では、ディレクトリ区切り文字として\または/を用いてRegex.Splitメソッドでファイルパスの分割を行っています。 このとき、区切りの正規表現\\|/をグループ化しているか・非キャプチャグループかどうかによってRegex.Splitメソッドの結果が変わります。

区切りの正規表現がグループ化されているかどうかでRegex.Splitメソッドの結果が変わる
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var path = "path/to/file.txt"; // ファイルパス

    // グループ化した正規表現を区切りとして分割する
    // (分割結果には区切りの文字列自体も含まれる)
    Console.WriteLine("[grouping]");
    foreach (var p in Regex.Split(path, @"(\\|/)")) {
      Console.WriteLine(p);
    }
    Console.WriteLine();

    // 非キャプチャグループ化した正規表現を区切りとして分割する
    // (分割結果に区切りの文字列が含まれなくなる)
    Console.WriteLine("[non-capturing group]");
    foreach (var p in Regex.Split(path, @"(?:\\|/)")) {
      Console.WriteLine(p);
    }
    Console.WriteLine();

    // グループ化していない正規表現を区切りとして分割する
    // (分割結果に区切りの文字列が含まれなくなる)
    Console.WriteLine("[non-grouping]");
    foreach (var p in Regex.Split(path, @"\\|/")) {
      Console.WriteLine(p);
    }
    Console.WriteLine();
  }
}
区切りの正規表現がグループ化されているかどうかでRegex.Splitメソッドの結果が変わる
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim path As String = "path/to/file.txt" ' ファイルパス

    ' グループ化した正規表現を区切りとして分割する
    ' (分割結果には区切りの文字列自体も含まれる)
    Console.WriteLine("[grouping]")
    For Each p As String In Regex.Split(path, "(\\|/)")
      Console.WriteLine(p)
    Next
    Console.WriteLine()

    ' 非キャプチャグループ化した正規表現を区切りとして分割する
    ' (分割結果に区切りの文字列が含まれなくなる)
    Console.WriteLine("[non-capturing group]")
    For Each p As String In Regex.Split(path, "(?:\\|/)")
      Console.WriteLine(p)
    Next
    Console.WriteLine()

    ' グループ化していない正規表現を区切りとして分割する
    ' (分割結果に区切りの文字列が含まれなくなる)
    Console.WriteLine("[non-grouping]")
    For Each p As String In Regex.Split(path, "\\|/")
      Console.WriteLine(p)
    Next
    Console.WriteLine()
  End Sub
End Class
実行結果
[grouping]
path
/
to
/
file.txt

[non-capturing group]
path
to
file.txt

[non-grouping]
path
to
file.txt

逆に言えば、Regex.Splitメソッドで区切り文字自体も分割結果に含めたい場合は、この動作を利用して区切りの正規表現をグループ化すれば・キャプチャさせればよいことになります。

さらに、明示的なグループ化を行っていない正規表現を区切りとして用いる場合は、RegexOptionsの指定によっても結果が変わることになります。 RegexOptions.ExplicitCaptureを指定すると明示的なグループ(名前付きのグループ)のみがキャプチャされることになるため、名前付きでないグループはグループ化されていない場合・非キャプチャグループの場合と同様に扱われます。

区切りの正規表現が明示的でないグループ化の場合、RegexOptions.ExplicitCaptureを指定するかどうかでRegex.Splitメソッドの結果が変わる
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var path = "path/to/file.txt"; // ファイルパス
    var pattern = @"(\\|/)"; // 区切りとして用いる正規表現(明示的ではないグループ)

    // 明示的ではないグループ化を行っている正規表現を区切りとして分割する
    // (RegexOptions.Noneでは、正規表現がグループ化されたものとして扱われるため、
    //  区切り文字自体も結果に含まれる)
    Console.WriteLine("[RegexOptions.None]");
    foreach (var p in Regex.Split(path, pattern, RegexOptions.None)) {
      Console.WriteLine(p);
    }

    // (RegexOptions.ExplicitCaptureでは、明示的でないグループはグループ化されていない場合と
    //  同等に扱われるため、区切り文字は結果に含まれない)
    Console.WriteLine("[RegexOptions.ExplicitCapture]");
    foreach (var p in Regex.Split(path, pattern, RegexOptions.ExplicitCapture)) {
      Console.WriteLine(p);
    }
  }
}
区切りの正規表現が明示的でないグループ化の場合、RegexOptions.ExplicitCaptureを指定するかどうかでRegex.Splitメソッドの結果が変わる
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim path As String = "path/to/file.txt" ' ファイルパス
    Dim pattern As String = "(\\|/)" ' 区切りとして用いる正規表現(明示的ではないグループ)

    ' 明示的ではないグループ化を行っている正規表現を区切りとして分割する
    ' (RegexOptions.Noneでは、正規表現がグループ化されたものとして扱われるため、
    '  区切り文字自体も結果に含まれる)
    Console.WriteLine("[RegexOptions.None]")
    For Each p As String In Regex.Split(path, pattern, RegexOptions.None)
      Console.WriteLine(p)
    Next

    ' (RegexOptions.ExplicitCaptureでは、明示的でないグループはグループ化されていない場合と
    '  同等に扱われるため、区切り文字は結果に含まれない)
    Console.WriteLine("[RegexOptions.ExplicitCapture]")
    For Each p As String In Regex.Split(path, pattern, RegexOptions.ExplicitCapture)
      Console.WriteLine(p)
    Next
  End Sub
End Class
実行結果
[RegexOptions.None]
path
/
to
/
file.txt
[RegexOptions.ExplicitCapture]
path
to
file.txt

グループ番号・グループ名の取得

正規表現内のグループの取得 (Regex.GetGroupNumbers/GetGroupNames)

Regex.GetGroupNumbersメソッドおよびRegex.GetGroupNamesメソッドを使用すると、正規表現内で定義されているグループ番号・グループ名の一覧を取得することができます。 このメソッドはインスタンスメソッドであるため、インスタンス化された正規表現に対してのみ使用できます。

GetGroupNumbersメソッドでは、正規表現内のすべてのグループの番号が返されます。 このメソッドでは、グループが名前付きグループかどうかに関わらず割り当てられているグループ番号が返されます。

GetGroupNamesメソッドでは、正規表現内のグループが名前付きグループの場合はその名前、そうでない場合は文字列形式のグループ番号が返されます。

どちらのメソッドの場合も、正規表現内にグループがあるかないかに関わらず、マッチ全体を表すグループ番号``0``が常に結果に含まれます。

Regex.GetGroupNumbers/GetGroupNamesメソッドで正規表現内で定義されているすべてのグループ番号/グループ名を取得する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var pattern = @"(?<year>\d{4})-(?<month>\d{2})-(\d{2})"; // yyyy-mm-dd

    // GetGroupNumbers/GetGroupNamesメソッドはインスタンスメソッドであるため、
    // インスタンス化された正規表現に対してのみ使用できる
    var re = new Regex(pattern);

    // GetGroupNumbersメソッドで正規表現内のグループ番号を取得する
    Console.WriteLine("group numbers: {0}", string.Join(", ", re.GetGroupNumbers()));

    // GetGroupNamesメソッドで正規表現内のグループ名を取得する
    // (名前付きグループの場合はその名前、そうでない場合は文字列形式のグループ番号が返される)
    Console.WriteLine("group names: {0}", string.Join(", ", re.GetGroupNames()));
  }
}
Regex.GetGroupNumbers/GetGroupNamesメソッドで正規表現内で定義されているすべてのグループ番号/グループ名を取得する
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim pattern As String = "(?<year>\d{4})-(?<month>\d{2})-(\d{2})" ' yyyy-mm-dd

    ' GetGroupNumbers/GetGroupNamesメソッドはインスタンスメソッドであるため、
    ' インスタンス化された正規表現に対してのみ使用できる
    Dim re As New Regex(pattern)

    ' GetGroupNumbersメソッドで正規表現内のグループ番号を取得する
    Console.WriteLine("group numbers: {0}", String.Join(", ", re.GetGroupNumbers()))

    ' GetGroupNamesメソッドで正規表現内のグループ名を取得する
    ' (名前付きグループの場合はその名前、そうでない場合は文字列形式のグループ番号が返される)
    Console.WriteLine("group names: {0}", String.Join(", ", re.GetGroupNames()))
  End Sub
End Class
実行結果
group numbers: 0, 1, 2, 3
group names: 0, 1, year, month

Regex.GroupNameFromNumber/GroupNumberFromNameメソッドを使うと、グループ番号に対応するグループ名、またその逆対応を取得することをできます。

グループ番号とグループ名の対応の取得 (Regex.GroupNameFromNumber/GroupNumberFromName)

Regex.GroupNameFromNumberメソッドおよびGroupNumberFromNameメソッドを使用すると、正規表現内で定義されているグループ番号に対応するグループ名、またその逆の対応を取得することができます。 このメソッドはインスタンスメソッドであるため、インスタンス化された正規表現に対してのみ使用できます。

存在しないグループ番号・グループ名を指定した場合、GroupNameFromNumberメソッドは空の文字列("")、GroupNumberFromNameメソッドは-1を返します。 GroupNumberFromNameメソッドは、文字列化したグループ番号をグループ名として指定することもできます。 このとき、名前付きグループでない場合はそのグループに対応するグループ番号が返され、名前付きのグループの場合は対応するグループが存在する場合でも-1が返されます。

Regex.GroupNameFromNumber/GroupNumberFromNameメソッドで正規表現内で定義されているグループ番号/グループ名の対応を取得する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var pattern = @"(?<year>\d{4})-(?<month>\d{2})-(\d{2})"; // yyyy-mm-dd

    // GetGroupNumbers/GetGroupNamesメソッドはインスタンスメソッドであるため、
    // インスタンス化された正規表現に対してのみ使用できる
    var re = new Regex(pattern);

    // GroupNameFromNumberメソッドで正規表現内のグループ名に対応するグループ番号を取得する
    Console.WriteLine("0 => '{0}'", re.GroupNameFromNumber(0));
    Console.WriteLine("1 => '{0}'", re.GroupNameFromNumber(1));
    Console.WriteLine("2 => '{0}'", re.GroupNameFromNumber(2));
    Console.WriteLine("3 => '{0}'", re.GroupNameFromNumber(3));
    Console.WriteLine("4 => '{0}'", re.GroupNameFromNumber(4)); // 存在しないグループ番号の場合は空の文字列が返される
    Console.WriteLine();

    // GroupNumberFromNameメソッドで正規表現内のグループ名に対応するグループ番号を取得する
    Console.WriteLine("year => {0}", re.GroupNumberFromName("year"));
    Console.WriteLine("month => {0}", re.GroupNumberFromName("month"));
    Console.WriteLine("0 => {0}", re.GroupNumberFromName("0")); // グループ番号を指定した場合、それが名前付きグループでなければ対応するグループ番号が返される
    Console.WriteLine("1 => {0}", re.GroupNumberFromName("1"));
    Console.WriteLine("2 => {0}", re.GroupNumberFromName("2")); // 名前付きグループのグループ番号を指定した場合、対応するグループが存在する場合でも-1が返される
    Console.WriteLine("non-existent => {0}", re.GroupNumberFromName("non-existent")); // 存在しないグループ名の場合は-1が返される
  }
}
Regex.GroupNameFromNumber/GroupNumberFromNameメソッドで正規表現内で定義されているグループ番号/グループ名の対応を取得する
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim pattern As String = "(?<year>\d{4})-(?<month>\d{2})-(\d{2})" ' yyyy-mm-dd

    ' GetGroupNumbers/GetGroupNamesメソッドはインスタンスメソッドであるため、
    ' インスタンス化された正規表現に対してのみ使用できる
    Dim re As New Regex(pattern)

    ' GroupNameFromNumberメソッドで正規表現内のグループ名に対応するグループ番号を取得する
    Console.WriteLine("0 => '{0}'", re.GroupNameFromNumber(0))
    Console.WriteLine("1 => '{0}'", re.GroupNameFromNumber(1))
    Console.WriteLine("2 => '{0}'", re.GroupNameFromNumber(2))
    Console.WriteLine("3 => '{0}'", re.GroupNameFromNumber(3))
    Console.WriteLine("4 => '{0}'", re.GroupNameFromNumber(4)) ' 存在しないグループ番号の場合は空の文字列が返される
    Console.WriteLine()

    ' GroupNumberFromNameメソッドで正規表現内のグループ名に対応するグループ番号を取得する
    Console.WriteLine("year => {0}", re.GroupNumberFromName("year"))
    Console.WriteLine("month => {0}", re.GroupNumberFromName("month"))
    Console.WriteLine("0 => {0}", re.GroupNumberFromName("0")) ' グループ番号を指定した場合、それが名前付きグループでなければ対応するグループ番号が返される
    Console.WriteLine("1 => {0}", re.GroupNumberFromName("1"))
    Console.WriteLine("2 => {0}", re.GroupNumberFromName("2")) ' 名前付きグループのグループ番号を指定した場合、対応するグループが存在する場合でも-1が返される
    Console.WriteLine("non-existent => {0}", re.GroupNumberFromName("non-existent")) ' 存在しないグループ名の場合は-1が返される
  End Sub
End Class
実行結果
0 => '0'
1 => '1'
2 => 'year'
3 => 'month'
4 => ''

year => 2
month => 3
0 => 0
1 => 1
2 => -1
non-existent => -1

マッチしたグループ名の取得 (Group.Name)

Group.Nameプロパティを参照すると、対応するグループ名を取得することができます。 Groupが名前を持たないグループを表す場合、Nameプロパティはグループ名の代わりにグループ番号を文字列形式で返します。 また、Groupがマッチ全体を表すグループを表す場合も、グループ番号0を文字列形式で返します。

Group.Nameプロパティは、.NET Framework 4.7以降・.NET Standard 2.1以降で使用することができます。 それ以前のバージョンではGroupインスタンスから直接グループ名を取得することはできないため、Regex.GroupNameFromNumberメソッドを使うなどする必要があります。

Group.Nameプロパティでグループの名前を取得する .NET Standard 2.1/.NET Framework 4.7
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "2016-02-29";
    var pattern = @"(?<year>\d{4})-(?<month>\d{2})-(\d{2})"; // yyyy-mm-dd

    var re = new Regex(pattern);
    var m = re.Match(text);

    if (m.Success) {
      foreach (Group g in m.Groups) {
        // Nameプロパティでマッチ元となったグループ名を取得する
        Console.WriteLine($"{g.Name} => {g.Value}");
      }
      Console.WriteLine();

      // .NET Framework 4.7/.NET Standard 2.1より前のバージョンではNameプロパティが存在しないため、
      // Regex.GroupNameFromNumberメソッドを使ってグループ番号からグループ名を取得する必要がある
      for (var number = 0; number < m.Groups.Count; number++) {
        Console.WriteLine($"{re.GroupNameFromNumber(number)} => {m.Groups[number].Value}");
      }
    }
  }
}
Group.Nameプロパティでグループの名前を取得する .NET Standard 2.1/.NET Framework 4.7
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim text As String = "2016-02-29"
    Dim pattern As String = "(?<year>\d{4})-(?<month>\d{2})-(\d{2})" ' yyyy-mm-dd

    Dim re As New Regex(pattern)
    Dim m As Match = re.Match(text)

    If m.Success Then
      For Each g As Group In m.Groups
        ' Nameプロパティでマッチ元となったグループ名を取得する
        Console.WriteLine($"{g.Name} => {g.Value}")
      Next
      Console.WriteLine()

      ' .NET Framework 4.7/.NET Standard 2.1より前のバージョンではNameプロパティが存在しないため、
      ' Regex.GroupNameFromNumberメソッドを使ってグループ番号からグループ名を取得する必要がある
      For number As Integer = 0 To m.Groups.Count - 1
        Console.WriteLine($"{re.GroupNameFromNumber(number)} => {m.Groups(number).Value}")
      Next
    End If
  End Sub
End Class
実行結果
0 => 2016-02-29
1 => 29
year => 2016
month => 02

0 => 2016-02-29
1 => 29
year => 2016
month => 02

キャプチャ(Captureクラス)

キャプチャとは、グループ化された正規表現にマッチした文字列の一つ一つを表します。 言い換えると、正規表現の部分式にマッチした箇所がキャプチャとなります。

例えば、3桁の数字の連続を表す正規表現(\d{3})+を考えた場合、この正規表現内のグループ(\d{3})には量指定子+があるため、グループ(\d{3})にマッチする文字列は複数となる場合があります。 この一つ一つがキャプチャとなります。 このように、グループに対して*, +{n}などの量指定子を指定した場合には、マッチした個々の文字列をキャプチャによって参照することができます。

キャプチャはCaptureクラスで扱います。 Captureクラスは、Groupクラスと同様にRegexクラスやMatchクラスから直接返されることはなく、Group.Capturesプロパティを通じて参照します。

グループ化された正規表現にマッチする箇所が複数ある場合、match.Groupsプロパティは各グループの最後にマッチした箇所=最後のキャプチャを表すGroupを返します。 一方group.Capturesプロパティは、マッチした個々の箇所に対応するCaptureを返します。 グループとは異なり、各キャプチャには0から始まるインデックスが割り当てられます。

Group.Capturesプロパティを参照して正規表現の部分式にマッチした箇所を列挙する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "0123456789";

    // 3桁の数字("(\d{3})")がひとつ以上連続("+")する箇所を取得
    var m = Regex.Match(text, @"(\d{3})+");

    // グループ1の正規表現("(\d{3})")にマッチした文字列を取得して表示
    // (最後にマッチした箇所が表示される)
    Console.WriteLine(m.Groups[1].Value);
    Console.WriteLine();

    // グループ1の正規表現にマッチした文字列(=キャプチャ)を列挙して表示
    foreach (Capture c in m.Groups[1].Captures) {
      Console.WriteLine(c.Value);
    }
  }
}
Group.Capturesプロパティを参照して正規表現の部分式にマッチした箇所を列挙する
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim text As String = "0123456789"

    ' 3桁の数字("(\d{3})")がひとつ以上連続("+")する箇所を取得
    Dim m As Match = Regex.Match(text, "(\d{3})+")

    ' グループ1の正規表現("(\d{3})")にマッチした文字列を取得して表示
    ' (最後にマッチした箇所が表示される)
    Console.WriteLine(m.Groups(1).Value)
    Console.WriteLine()

    ' グループ1の正規表現にマッチした文字列(=キャプチャ)を列挙して表示
    For Each c As Capture In m.Groups(1).Captures
      Console.WriteLine(c.Value)
    Next
  End Sub
End Class
実行結果
678

012
345
678

キャプチャを使った例として、カンマ区切り文字列(CSV)から各カラムの値を分割して取得する例を考えます。

次の例で使用している正規表現((^|,)(?<column>[^,]*))+では、行頭またはカンマ(^|,)に続いて、グループ名columnでグループ化した正規表現(?<column>[^,]*)が現れる箇所の組み合わせが、1つ以上連続する箇所(...)+をCSVにマッチする正規表現として構成しています。

そして、グループcolumnでは、カンマ以外の文字が0文字以上連続する箇所[^,]*をカラムの値として構成しています。 この一連の正規表現にマッチする文字列を取得し、グループcolumnにキャプチャされた文字列を列挙すれば、CSVの各カラムの値を取得できることになります。

グループ化した正規表現とキャプチャを使ってCSV文字列から各カラムの値を取得する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "a,012,,xxxxx";

    // CSVの正規表現(グループcolumnにCSVの各カラムの値がキャプチャされる)
    var m = Regex.Match(text, @"((^|,)(?<column>[^,]*))+");

    // グループcolumnの正規表現にキャプチャされた数を表示
    Console.WriteLine("Count = {0}", m.Groups["column"].Captures.Count);

    // グループcolumnにキャプチャされた文字列を列挙して表示
    foreach (Capture c in m.Groups["column"].Captures) {
      Console.WriteLine("\"{0}\"", c.Value);
    }
  }
}
グループ化した正規表現とキャプチャを使ってCSV文字列から各カラムの値を取得する
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim text As String = "a,012,,xxxxx"

    ' CSVの正規表現(グループcolumnにCSVの各カラムの値がキャプチャされる)
    Dim m As Match = Regex.Match(text, "((^|,)(?<column>[^,]*))+")

    ' グループcolumnの正規表現にキャプチャされた数を表示
    Console.WriteLine("Count = {0}", m.Groups("column").Captures.Count)

    ' グループcolumnにキャプチャされた文字列を列挙して表示
    For Each c As Capture In m.Groups("column").Captures
      Console.WriteLine("""{0}""", c.Value)
    Next
  End Sub
End Class
実行結果
Count = 4
"a"
"012"
""
"xxxxx"

この例で使用している正規表現では、クォーテーション("')によって括られたカラムを処理することができません。 CSVをより厳密に処理するバージョンについては§.グループとキャプチャを使った例 (CSVの処理)で紹介しています。

Match・Group・Capture各クラスの関係とマッチ箇所

MatchクラスGroupクラスCaptureクラスの違いとそれぞれの関係を整理します。 それぞれのクラスが表す箇所(部分文字列)は次のようになります。

Match
正規表現全体にマッチした箇所を表す
Group
正規表現の個々のグループにマッチした箇所を表す
Capture
正規表現の部分式(グループ化された正規表現)にマッチしたすべての箇所を表す

例えば、Windows形式のフルパスにマッチする正規表現([A-Z]:)(\\[^\\ ]+)+を考えた場合、各クラスが表す部分文字列は次のようになります。

Match
正規表現([A-Z]:)(\\[^\\ ]+)+の全体にマッチした箇所
Group
グループ化された正規表現([A-Z]:)(\\[^\\ ]+)+のそれぞれにマッチした箇所
Capture
グループ化された正規表現内の部分式[A-Z]:\\[^\\ ]+にマッチしたすべての箇所

以下、入力文字列xcopy C:\test D:\target\backup\filesを例にとり、正規表現([A-Z]:)(\\[^\\ ]+)+とMatch・Group・Captureのそれぞれが表す箇所を図示します。 この正規表現は、入力文字列のうちC:\testD:\target\backup\filesの部分がマッチします。

このとき、Matchクラスが表す箇所とインデックスは次のようになります。

Matchが表す個所
x c o p y   C : \ t e s t   D : \ t a r g e t \ b a c k u p \ f i l e s
            |           |   |                                         |
            `-----0-----'   `--------------------1--------------------'

同様に、各MatchクラスのGroupsプロパティに含まれるGroupが表す箇所とインデックスは次のようになります。 グループ番号0のグループは、マッチした箇所全体を表す点に注意してください。

Groupが表す箇所
            |<-Match[0]>|   |<----------------Match[1]--------------->|
            |           |   |                                         |
x c o p y   C : \ t e s t   D : \ t a r g e t \ b a c k u p \ f i l e s
            | | |       |   | | |                                     |
            `1' `---2---'   `1' `-----------------2-------------------'
            |           |   |                                         |
            `-----0-----'   `--------------------0--------------------'

最後に、各GroupクラスのCapturesプロパティに含まれるCaptureが表す箇所とインデックスは次のようになります。 グループとは異なり、インデックス0は最初のキャプチャを表す点に注意してください。

Captureが表す箇所
          Group[1]        Group[1]
            |-|  Group[2]   |-|
            | | |<----->|   | | |<-------------Group[2]-------------->|
            | | |       |   | | |                                     |
x c o p y   C : \ t e s t   D : \ t a r g e t \ b a c k u p \ f i l e s
            | | |       |   | | |           | |           | |         |
            `0' `---0---'   `0' `-----0-----' `-----1-----' `----2----'

Match・Group・Captureと、実際にマッチする部分をツリー形式で表すと次のようになります。 カッコ内は入力文字列中におけるマッチした部分のインデックスとその長さです。

実行結果
Regex: ([A-Z]:)(\\[^\\ ]+)+
Input: xcopy C:\test D:\target\backup\files

Match[0]         C:\test                   (6+7)
  Group[0]       C:\test                   (6+7)
    Capture[0]   C:\test                   (6+7)
  Group[1]       C:                        (6+2)
    Capture[0]   C:                        (6+2)
  Group[2]       \test                     (8+5)
    Capture[0]   \test                     (8+5)

Match[1]         D:\target\backup\files    (14+22)
  Group[0]       D:\target\backup\files    (14+22)
    Capture[0]   D:\target\backup\files    (14+22)
  Group[1]       D:                        (14+2)
    Capture[0]   D:                        (14+2)
  Group[2]       \files                    (30+6)
    Capture[0]   \target                   (16+7)
    Capture[1]   \backup                   (23+7)
    Capture[2]   \files                    (30+6)

Match・Group・Captureの各内容をツリー形式で表示する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = @"xcopy C:\test D:\target\backup\files";
    var pattern = @"([A-Z]:)(\\[^\\ ]+)+";

    Console.WriteLine("Regex: {0}", pattern);
    Console.WriteLine("Input: {0}", text);

    Console.WriteLine();

    PrintMatch(Regex.Matches(text, pattern));
  }

  static void PrintMatch(MatchCollection matches)
  {
    for (var i = 0; i < matches.Count; i++) {
      Match m = matches[i];

      Console.WriteLine("Match[{0}]         {1,-25} ({2}+{3})", i, m.Value, m.Index, m.Length);

      PrintGroup(m.Groups);

      Console.WriteLine();
    }
  }

  static void PrintGroup(GroupCollection groups)
  {
    for (var i = 0; i < groups.Count; i++) {
      Group g = groups[i];

      Console.WriteLine("  Group[{0}]       {1,-25} ({2}+{3})", i, g.Value, g.Index, g.Length);

      PrintCapture(g.Captures);
    }
  }

  static void PrintCapture(CaptureCollection captures)
  {
    for (var i = 0; i < captures.Count; i++) {
      Capture c = captures[i];

      Console.WriteLine("    Capture[{0}]   {1,-25} ({2}+{3})", i, c.Value, c.Index, c.Length);
    }
  }
}

Match・Group・Captureの継承関係とコレクションクラスの対応

Match・Group・Captureの各クラスは次のような継承関係となっています。 ValueIndexLengthの各プロパティは、すべてCaptureクラスから継承されます。

また、それぞれのクラスと取得できるメソッド・プロパティ、対応するコレクションクラスの関係は次のようになっています。

Match・Group・Captureと対応するコレクションクラス
クラス 取得できるメソッド・プロパティ 対応するコレクションクラス
Matchクラス Regex.Matchesメソッド MatchCollectionクラス
Groupクラス Match.Groupsプロパティ GroupCollectionクラス
Captureクラス Group.Capturesプロパティ CaptureCollectionクラス

MatchCollection・GroupCollection・CaptureCollectionとジェネリックコレクションインターフェイス

MatchCollectionGroupCollectionCaptureCollectionの各コレクションは、当初非ジェネリックなIEnumerableインターフェイスのみを実装する型指定されていないコレクションとして登場し、その後.NET Standard 2.1/.NET Core 2.0にてIEnumerable<T>/ICollection<T>/IList<T>を実装するようになり、型指定されたコレクションとして扱うことができるようになっています。

一方、Match/Group/CaptureCollectionのGetEnumeratorメソッドは、おそらくAPI互換性維持の観点から、IEnumerable<T>の実装以降も非ジェネリックなIEnumeratorを返す実装のままとなっています。 このためC#では、これらのコレクションクラスから取得した列挙子IEnumeratorで列挙する場合、varによる暗黙的な型指定を行うとobjectとして列挙されます

Match/Group/CaptureCollectionは非ジェネリックなIEnumeratorを返すため暗黙的な型指定ではobjectとして列挙される
using System;
using System.Text.RegularExpressions;

class Sample {
  static void PrintMatches(MatchCollection matches)
  {
    // MatchCollectionでは非ジェネリックなIEnumeratorを返すため、
    // 列挙する際に暗黙的な型指定を行うと、objectとして扱われる
    foreach (/*Match*/ var match in matches) {
      // matchの型はobjectとして扱われるため、Valueプロパティを参照できない
      Console.WriteLine(match.Value); // error CS1061: 'object' に 'Value' の定義が含まれておらず、型 'object' の最初の引数を受け付けるアクセス可能な拡張メソッド 'Value' が見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足していないことを確認してください。

      PrintGroups(match.Groups); // error CS1061: 'object' に 'Groups' の定義が含まれておらず、型 'object' の最初の引数を受け付けるアクセス可能な拡張メソッド 'Groups' が見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足していないことを確認してください。
    }
  }

  static void PrintGroups(GroupCollection groups)
  {
    // GroupCollectionの場合もMatchCollectionと同様
    foreach (/*Group*/ var group in groups) {
      Console.WriteLine(group.Value); // error CS1061: 'object' に 'Value' の定義が含まれておらず、型 'object' の最初の引数を受け付けるアクセス可能な拡張メソッド 'Value' が見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足していないことを確認してください。

      PrintCaptures(group.Captures); // error CS1061: 'object' に 'Captures' の定義が含まれておらず、型 'object' の最初の引数を受け付けるアクセス可能な拡張メソッド 'Captures' が見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足していないことを確認してください。
    }
  }

  static void PrintCaptures(CaptureCollection captures)
  {
    // CaptureCollectionの場合もMatchCollectionと同様
    foreach (/*Capture*/ var capture in captures) {
      Console.WriteLine(capture.Value); // error CS1061: 'object' に 'Value' の定義が含まれておらず、型 'object' の最初の引数を受け付けるアクセス可能な拡張メソッド 'Value' が見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足していないことを確認してください。
    }
  }

  static void Main()
  {
    PrintMatches(Regex.Matches("text", "pattern"));
  }
}

したがって、これらのコレクションをforeachで列挙を行う際には、明示的に型指定する、IEnumerable<Match>等にキャストしてから列挙する、LINQのOfType<Match>()メソッドCast<Match>()メソッドを用いて型変換してから列挙する、などする必要があります。

Match/Group/CaptureCollectionから直接ではなく、ジェネリックインターフェイスを介してGetEnumeratorメソッドを呼び出す場合は、ジェネリックな(型指定された)列挙子IEnumerator<T>を取得できます。 そのため、これらのコレクションを引数としてとる場合は、Match/Group/CaptureCollectionの代わりにIEnumerable/IReadOnlyCollection/IReadOnlyList<Match/Group/Capture>などで受けるようにすることもできます。

Match/Group/CaptureCollectionをIReadOnlyList<Match/Group/Capture>インターフェイスを介して列挙する .NET Standard 2.1/.NET Core 2.0
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

class Sample {
  // MatchCollection直接ではなくIReadOnlyList<Match>として引き受ける
  static void PrintMatches(IReadOnlyList<Match> matches)
  {
    // IReadOnlyList<Match>を介して列挙することで、型指定されたIEnumerator<Match>によって列挙することができる
    foreach (var match in matches) {
      Console.WriteLine(match.Value);

      PrintGroups(match.Groups);
    }
  }

  // GroupCollectionの場合もMatchCollectionと同様
  static void PrintGroups(IReadOnlyList<Group> groups)
  {
    foreach (var group in groups) {
      Console.WriteLine(group.Value);

      PrintCaptures(group.Captures);
    }
  }

  // CaptureCollectionの場合もMatchCollectionと同様
  static void PrintCaptures(IReadOnlyList<Capture> captures)
  {
    foreach (var capture in captures) {
      Console.WriteLine(capture.Value);
    }
  }

  static void Main()
  {
    PrintMatches(Regex.Matches("text", "pattern"));
  }
}

IEnumerable・IEnumeratorインターフェイスと列挙について詳しくはIEnumerable・IEnumeratorを参照してください。

GroupCollectionとIReadOnlyDictionary

.NET Core 3.0以降のGroupCollectionは、IReadOnlyDictionary<string, Group>インターフェイスも実装しています。 これにしたがって、GroupCollectionをディクショナリとして扱えるほか、グループ名前付きグループKeyValuePair<string, Group>として取得することができます。

IReadOnlyDictionaryインターフェイスを介してGroupCollectionを列挙する
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var text = "2020-09-01";
    // yyyy-mm-dd形式の日付にマッチする正規表現
    // (yyyy部分・mm部分はそれぞれ名前'y'と'm'としてグループ化、dd部分は名前なしでグループ化)
    var pattern = @"(?<y>\d{4})-(?<m>\d{2})-(\d{2})";

    var m = Regex.Match(text, pattern);

    // マッチしたグループを、KeyValuePair<string, Group>として列挙する
    foreach (var (name, group) in (IReadOnlyDictionary<string, Group>)m.Groups) {
      Console.WriteLine($"{name} => {group.Value}");
    }
  }
}
IReadOnlyDictionaryインターフェイスを介してGroupCollectionを列挙する
Imports System
Imports System.Collections.Generic
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim text As String = "2020-09-01"
    ' yyyy-mm-dd形式の日付にマッチする正規表現
    ' (yyyy部分・mm部分はそれぞれ名前'y'と'm'としてグループ化、dd部分は名前なしでグループ化)
    Dim pattern As String = "(?<y>\d{4})-(?<m>\d{2})-(\d{2})"

    Dim m As Match = Regex.Match(text, pattern)

    ' マッチしたグループを、KeyValuePair(Of String, Group)として列挙する
    For Each pair As KeyValuePair(Of String, Group) In DirectCast(m.Groups, IReadOnlyDictionary(Of String, Group))
      Console.WriteLine($"{pair.Key} => {pair.Value.Value}")
    Next
  End Sub
End Class
実行結果
0 => 2020-09-01
1 => 01
y => 2020
m => 09

上記の結果にも現れているように、IReadOnlyDictionary<string, Group>を介したアクセスの場合、名前なし・名前付きに関わらずグループ化された部分が列挙されます。 また、名前なしのグループでは、グループ番号は文字列のキーとして列挙され、マッチ箇所全体を表すグループとして"0"が常に含まれます。

このほかにもGroupCollectionでIReadOnlyDictionary由来のメソッド・プロパティとして、KeysプロパティContainsKeyメソッドTryGetValueメソッドを使うこともできます。 これらのメソッド・プロパティは、正規表現にマッチしたかどうかで返す値が変わります。 具体的には、ContainsKeyメソッド・TryGetValueメソッドは、正規表現にマッチした場合、かつ正規表現内に指定された名前・番号のグループを含む場合のみtrueを返します。 つまり、特定の名前・番号のグループにマッチしたかどうか表す値を返すのではないため、逆にいえばこのメソッドの戻り値から正規表現全体がマッチしたかどうかを知ることができます。

GroupCollection.TryGetValueメソッドを使って正規表現にマッチした場合にグループ化した箇所を取得する
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    // 整数表現にマッチする正規表現
    // (符号部分を名前'sign'としてグループ化、絶対値部分を名前'number'としてグループ化)
    var pattern = @"(?<sign>\-|\+)?(?<number>[0-9]+)";

    foreach (var text in new[] {"+123", "45", "abc"}) {
      var m = Regex.Match(text, pattern);

      Console.Write($"{text} => {(m.Success ? "match" : "not match")} ");

      // 正規表現にマッチした場合は、グループ'sign'の部分を表示する
      if (m.Groups.TryGetValue("sign", out var sign))
        Console.Write($"sign='{sign.Value}' ");

      // 正規表現にマッチした場合は、グループ'number'の部分を表示する
      if (m.Groups.TryGetValue("number", out var number))
        Console.Write($"number='{number.Value}' ");

      Console.WriteLine();
    }
  }
}
GroupCollection.TryGetValueメソッドを使って正規表現にマッチした場合にグループ化した箇所を取得する
Imports System
Imports System.Collections.Generic
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    ' 整数表現にマッチする正規表現
    ' (符号部分を名前'sign'としてグループ化、絶対値部分を名前'number'としてグループ化)
    Dim pattern As String = "(?<sign>\-|\+)?(?<number>[0-9]+)"

    For Each text As String In New String() {"+123", "45", "abc"}
      Dim m As Match = Regex.Match(text, pattern)

      Console.Write($"{text} => {If(m.Success, "match", "not match")} ")

      ' 正規表現にマッチした場合は、グループ'sign'の部分を表示する
      Dim sign As Group = Nothing

      If m.Groups.TryGetValue("sign", sign) Then
        Console.Write($"sign='{sign.Value}' ")
      End If

      ' 正規表現にマッチした場合は、グループ'number'の部分を表示する
      Dim number As Group = Nothing

      If m.Groups.TryGetValue("number", number) Then
        Console.Write($"number='{number.Value}' ")
      End If

      Console.WriteLine()
    Next
  End Sub
End Class
実行結果
+123 => match sign='+' number='123' 
45 => match sign='' number='45' 
abc => not match 

グループとキャプチャを使った例 (CSVの処理)

以下の例では、名前付きグループとキャプチャを使ってCSVの各レコードからフィールドの値を抽出し、表形式に整形しています。 この例で使用する正規表現は複雑になるため、構成要素毎に分けた正規表現を組み立てています。 それぞれの正規表現は次のようになっています。

recordPattern = ((^|,)fieldPattern)*$
レコード(1行分)のパターン
行頭^またはカンマ,の後にfieldPatternが続く組み合わせが0個以上続き(*)、最後に行末$が出現する文字列で構成される
fieldPattern = (quotedFieldPattern|plainFieldPattern)
レコード内の各フィールドのパターン
クオートされたフィールドquotedFieldPattern、もしくはクオートされていないフィールドplainFieldPatternで構成される
quotedFieldPattern = "(?<value>(""|[^"])*?)"
クオートされているフィールドのパターン
開きの二重引用符"、クオートされた値((?<value>...)の部分)、閉じの二重引用符"で構成される
クオートされた値は、エスケープされた二重引用符""または閉じの二重引用符以外の文字[^"]が0個以上続く(*?)文字列で構成される
(ここでは(""|[^"])が閉じの二重引用符にマッチしないよう、*ではなく最短一致の*?を使用する)
plainFieldPattern = (?<value>[^,]*)
クオートされていないフィールドのパターン
カンマ以外の文字([^,])が0個以上続く(*)文字列で構成される

なお、最終的に組み立てられた正規表現は実行結果として出力しています。 また、この例で使用している正規表現では、名前付きグループvalueに複数のフィールドがキャプチャされるため、Group.Capturesプロパティを参照してキャプチャしたフィールドの値を列挙しています。

正規表現を使ってクオートを含むCSVの各レコードからフィールドの値を抽出する
using System;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    var plainFieldPattern  = "(?<value>[^,]*)";
    var quotedFieldPattern = "\"(?<value>(\"\"|[^\"])*?)\"";
    var fieldPattern       = $"({quotedFieldPattern}|{plainFieldPattern})";
    var recordPattern      = $"((^|,){fieldPattern})*$";

    Console.WriteLine(recordPattern);
    Console.WriteLine();

    var records = new[] {
      "type,example,max value",
      "",
      "int,\"0, 16, 42\",int.MaxValue",
      "double,3.14,double.MaxValue",
      "bool,\"true, false\",",
      "string,\"\"\"foo\"\", \"\"bar\"\"\",",
    };

    foreach (var record in records) {
      Console.WriteLine("<{0}>", record);
    }

    Console.WriteLine();

    foreach (var record in records) {
      var m = Regex.Match(record, recordPattern);

      if (m.Success) {
        foreach (Capture c in m.Groups["value"].Captures) {
          Console.Write("|{0,-20}", c.Value.Replace("\"\"", "\""));
        }
      }

      Console.WriteLine();
    }
  }
}
正規表現を使ってクオートを含むCSVの各レコードからフィールドの値を抽出する
Imports System
Imports System.Text.RegularExpressions

Class Sample
  Shared Sub Main()
    Dim plainFieldPattern As String   = "(?<value>[^,]*)"
    Dim quotedFieldPattern As String  = """(?<value>(""""|[^""])*?)"""
    Dim fieldPattern As String        = $"({quotedFieldPattern}|{plainFieldPattern})"
    Dim recordPattern As String       = $"((^|,){fieldPattern})*$"

    Console.WriteLine(recordPattern)
    Console.WriteLine()

    Dim records() As String = New String() { _
      "type,example,max value", _
      "", _
      "int,""0, 16, 42"",int.MaxValue", _
      "double,3.14,double.MaxValue", _
      "bool,""true, false"",", _
      "string,""""""foo"""", """"bar""""""," _
    }

    For Each record As String In records
      Console.WriteLine("<{0}>", record)
    Next

    Console.WriteLine()

    For Each record As String In records
      Dim m As Match = Regex.Match(record, recordPattern)

      If m.Success Then
        For Each c As Capture In m.Groups("value").Captures
          Console.Write("|{0,-20}", c.Value.Replace("""""", """"))
        Next
      End If

      Console.WriteLine()
    Next
  End Sub
End Class
実行結果
((^|,)("(?<value>(""|[^"])*?)"|(?<value>[^,]*)))*$

<type,example,max value>
<>
<int,"0, 16, 42",int.MaxValue>
<double,3.14,double.MaxValue>
<bool,"true, false",>
<string,"""foo"", ""bar""",>

|type                |example             |max value           
|                    
|int                 |0, 16, 42           |int.MaxValue        
|double              |3.14                |double.MaxValue     
|bool                |true, false         |                    
|string              |"foo", "bar"        |