.NET 正規表現ソース ジェネレーター

正規表現 (regex) は、開発者が検索するパターンを表現できる文字列であり、テキストを検索し、検索対象の文字列から結果をサブセットとして抽出するための、非常に一般的な方法です。 .NET では、System.Text.RegularExpressions 名前空間を使って Regex のインスタンスと静的メソッドを定義し、ユーザーが定義したパターンと一致させます。 この記事では、ソース生成を使用して Regex のインスタンスを生成し、パフォーマンスを最適化する方法について説明します。

Note

可能であれば、RegexOptions.Compiled オプションを使用して正規表現をコンパイルするのでなく、ソースで生成された正規表現を使用してください。 ソース生成を行うと、アプリの起動および実行がより迅速になり、トリミングもしやすくなります。 ソース生成が可能な場合については、「いつ使用するか」を参照してください。

コンパイルされた正規表現

new Regex("somepattern") を記述すると、いくつかのことが起こります。 指定したパターンが解析され、パターンの有効性が保証されると共に、解析された正規表現を表す内部ツリーに変換されます。 その後、ツリーはさまざまな方法で最適化され、より効率的に実行できる機能的に同等のバリエーションにパターンが変換されます。 ツリーは、照合方法に関する命令を正規表現インタープリター エンジンに提供する、一連のオペコードとオペランドとして解釈できる形式で記述されます。 照合の実行時には、インタープリターはそれらの命令を順番に見ながら、入力テキストに対する処理を行います。 インタープリターは、新しい Regex インスタンスをインスタンス化するとき、またはRegex で静的メソッドのいずれかを呼び出すときに、既定で使われるエンジンです。

RegexOptions.Compiled を指定すると、同じ構築時の処理がすべて実行されます。 結果の命令は、リフレクション出力ベースのコンパイラによって、いくつかの DynamicMethod に書き込まれる IL 命令にさらに変換されます。 照合が実行されるときに、それらの DynamicMethod が呼び出されます。 この IL は、処理される正確なパターンに特化されている点を除き、基本的に、インタープリターが行うとおりのことを行います。 たとえば、パターンに [ac] が含まれる場合、インタープリターが認識するオペコードは、"現在位置の入力文字を、このセットの説明で指定されているセットに対して照合する" という内容になります。一方、コンパイルされた IL に含まれるコードは、実質的に、"現在位置の入力文字を、'a' または 'c' に対して照合する" という内容になります。 この特殊なケーシングと、パターンの知識に基づいて最適化を実行する機能は、RegexOptions.Compiled を指定すると、インタープリターよりはるかに速い照合スループットが得られる主な理由の一部です。

RegexOptions.Compiled にはいくつかの欠点があります。 最も影響が大きいのは、インタープリターを使う場合よりはるかに多くの構築コストが発生することです。 インタープリターと同じコストがすべてかかるだけでなく、結果の RegexNode ツリーおよび生成されたオペコードとオペランドを IL にコンパイルする必要があり、これにより些細とはいえないコストが加わります。 生成された IL を最初に使用するときにさらに JIT コンパイルする必要があり、開始時にはさらに多くのコストが発生します。 RegexOptions.Compiled は、最初の使用時のオーバーヘッドと、後続のすべての使用時のオーバーヘッドの間の、基本的なトレードオフを表します。 また、System.Reflection.Emit を使うと、特定の環境では RegexOptions.Compiled を使用できなくなります。一部のオペレーティング システムでは、動的に生成されたコードの実行が許可されず、そのようなシステムでは Compiled は動作しなくなります。

ソース生成

.NET 7 では、新しい RegexGenerator ソース ジェネレーターが導入されました。 C# コンパイラが "Roslyn" C# コンパイラとして書き換えられたときに、コンパイル パイプライン全体とアナライザーに対してオブジェクト モデルが公開されました。 最近では、Roslyn でソース ジェネレーターが有効になりました。 アナライザーと同様に、ソース ジェネレーターはコンパイラに接続するコンポーネントであり、アナライザーと同じ情報をすべて渡されますが、診断を出力できるだけでなく、追加のソース コードでコンパイル ユニットを拡張することもできます。 .NET 7+ SDK には、Regex を返す部分メソッド上で新しい GeneratedRegexAttribute を認識する、新しいソース ジェネレーターが含まれます。 そのソース ジェネレーターは、Regex のためのすべてのロジックを実装する、そのメソッドの実装を提供します。 たとえば、次のようなコードを記述したとします。

private static readonly Regex s_abcOrDefGeneratedRegex =
    new(pattern: "abc|def",
        options: RegexOptions.Compiled | RegexOptions.IgnoreCase);

private static void EvaluateText(string text)
{
    if (s_abcOrDefGeneratedRegex.IsMatch(text))
    {
        // Take action with matching text
    }
}

このコードは、次のように書き換えることができます。

[GeneratedRegex("abc|def", RegexOptions.IgnoreCase, "en-US")]
private static partial Regex AbcOrDefGeneratedRegex();

private static void EvaluateText(string text)
{
    if (AbcOrDefGeneratedRegex().IsMatch(text))
    {
        // Take action with matching text
    }
}

生成された AbcOrDefGeneratedRegex() の実装では、同様にシングルトン Regex インスタンスがキャッシュされるため、コードを使うために追加のキャッシュは必要ありません。

ヒント

この RegexOptions.Compiled フラグはソース ジェネレーターによって無視されるため、ソース生成バージョンでは不要になります。

次の画像は、ソースが生成されてキャッシュされたインスタンス (ソース ジェネレーターが出力する internalRegex サブクラス) のスクリーン キャプチャです。

キャッシュされた RegEx の静的フィールド

しかし、見てわかるように、それは単に new Regex(...) を行うのではありません. そうではなく、ソース ジェネレーターは、IL で RegexOptions.Compiled が出力するとの同様のロジックを含むカスタム Regex 派生実装を、C# コードとして出力します。 スループット パフォーマンスに関するRegexOptions.Compiled のすべての利点と (実際にはそれ以上)、起動時の Regex.CompileToAssembly の利点を、CompileToAssembly の複雑さなしで得られます。 出力されるソースはプロジェクトの一部であるため、簡単に表示およびデバッグすることもできます。

ソース生成された Regex コードによるデバッグ

ヒント

Visual Studio で、部分メソッドの宣言を右クリックして、[定義に移動] を選びます。 または、ソリューション エクスプローラーでプロジェクト ノードを選び、[依存関係]>[アナライザー]>System.Text.RegularExpressions.Generator>System.Text.RegularExpressions.Generator.RegexGenerator>RegexGenerator.g.cs を展開して、この正規表現ジェネレーターから生成された C# コードを表示します。

それにブレークポイントを設定したり、それをステップ実行したり、それを学習ツールとして使って正規表現エンジンが入力でパターンを処理する方法を正確に理解したりできます。 さらに、ジェネレーターはスラッシュ 3 つの (XML) コメントを生成するので、式を一目で理解したり、使用する場所を理解したりするのに役立ちます。

正規表現について説明している、生成された XML コメント

ソースで生成されるファイルの内容

.NET 7 では、ソース ジェネレーターと RegexCompiler がほぼ完全に書き換えられ、生成されるコードの構造が根本的に変更されました。 このアプローチは、すべてのコンストラクトを処理するように拡張されており (1 つ注意点があります)、RegexCompiler とソース ジェネレーターはどちらもまだ、新しいアプローチに従って、ほぼ 1 対 1 に互いに対応します。 (a|bc)d 式からの主要な関数の 1 つに対するソース ジェネレーターの出力を検討します。

private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
    int pos = base.runtextpos;
    int matchStart = pos;
    ReadOnlySpan<char> slice = inputSpan.Slice(pos);

    // Match with 2 alternative expressions, atomically.
    {
        if (slice.IsEmpty)
        {
            return false; // The input didn't match.
        }

        switch (slice[0])
        {
            case 'A' or 'a':
                if ((uint)slice.Length < 3 ||
                    !slice.Slice(1).StartsWith("bc", StringComparison.OrdinalIgnoreCase)) // Match the string "bc" (ordinal case-insensitive)
                {
                    return false; // The input didn't match.
                }

                pos += 3;
                slice = inputSpan.Slice(pos);
                break;

            case 'D' or 'd':
                if ((uint)slice.Length < 3 ||
                    !slice.Slice(1).StartsWith("ef", StringComparison.OrdinalIgnoreCase)) // Match the string "ef" (ordinal case-insensitive)
                {
                    return false; // The input didn't match.
                }

                pos += 3;
                slice = inputSpan.Slice(pos);
                break;

            default:
                return false; // The input didn't match.
        }
    }

    // The input matched.
    base.runtextpos = pos;
    base.Capture(0, matchStart, pos);
    return true;
}
private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
    int pos = base.runtextpos;
    int matchStart = pos;
    int capture_starting_pos = 0;
    ReadOnlySpan<char> slice = inputSpan.Slice(pos);

    // 1st capture group.
    //{
        capture_starting_pos = pos;

        // Match with 2 alternative expressions.
        //{
            if (slice.IsEmpty)
            {
                UncaptureUntil(0);
                return false; // The input didn't match.
            }

            switch (slice[0])
            {
                case 'a':
                    pos++;
                    slice = inputSpan.Slice(pos);
                    break;

                case 'b':
                    // Match 'c'.
                    if ((uint)slice.Length < 2 || slice[1] != 'c')
                    {
                        UncaptureUntil(0);
                        return false; // The input didn't match.
                    }

                    pos += 2;
                    slice = inputSpan.Slice(pos);
                    break;

                default:
                    UncaptureUntil(0);
                    return false; // The input didn't match.
            }
        //}

        base.Capture(1, capture_starting_pos, pos);
    //}

    // Match 'd'.
    if (slice.IsEmpty || slice[0] != 'd')
    {
        UncaptureUntil(0);
        return false; // The input didn't match.
    }

    // The input matched.
    pos++;
    base.runtextpos = pos;
    base.Capture(0, matchStart, pos);
    return true;

    // <summary>Undo captures until it reaches the specified capture position.</summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    void UncaptureUntil(int capturePosition)
    {
        while (base.Crawlpos() > capturePosition)
        {
            base.Uncapture();
        }
    }
}

ソース生成コードの目標は、わかりやすい構造、各ステップで何が行われているかを説明するコメント、および一般にジェネレーターは人が書いたかのようなコードを出力する必要があるという指針の下で出力されたコードによって、理解しやすいものにすることです。 バックトラッキングが関係している場合でも、バックトラッキングの構造は、スタックに依存して次にジャンプする場所を示すのではなく、コードの構造の一部になります。 たとえば、式が [ab]*[bc] である場合に生成される同じ照合関数のコードを次に示します。

private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
    int pos = base.runtextpos;
    int matchStart = pos;
    int charloop_starting_pos = 0, charloop_ending_pos = 0;
    ReadOnlySpan<char> slice = inputSpan.Slice(pos);

    // Match a character in the set [ABab] greedily any number of times.
    //{
        charloop_starting_pos = pos;

        int iteration = slice.IndexOfAnyExcept(Utilities.s_ascii_600000006000000);
        if (iteration < 0)
        {
            iteration = slice.Length;
        }

        slice = slice.Slice(iteration);
        pos += iteration;

        charloop_ending_pos = pos;
        goto CharLoopEnd;

        CharLoopBacktrack:

        if (Utilities.s_hasTimeout)
        {
            base.CheckTimeout();
        }

        if (charloop_starting_pos >= charloop_ending_pos ||
            (charloop_ending_pos = inputSpan.Slice(charloop_starting_pos, charloop_ending_pos - charloop_starting_pos).LastIndexOfAny(Utilities.s_ascii_C0000000C000000)) < 0)
        {
            return false; // The input didn't match.
        }
        charloop_ending_pos += charloop_starting_pos;
        pos = charloop_ending_pos;
        slice = inputSpan.Slice(pos);

        CharLoopEnd:
    //}

    // Advance the next matching position.
    if (base.runtextpos < pos)
    {
        base.runtextpos = pos;
    }

    // Match a character in the set [BCbc].
    if (slice.IsEmpty || ((uint)((slice[0] | 0x20) - 'b') > (uint)('c' - 'b')))
    {
        goto CharLoopBacktrack;
    }

    // The input matched.
    pos++;
    base.runtextpos = pos;
    base.Capture(0, matchStart, pos);
    return true;
}
private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
    int pos = base.runtextpos;
    int matchStart = pos;
    int charloop_starting_pos = 0, charloop_ending_pos = 0;
    ReadOnlySpan<char> slice = inputSpan.Slice(pos);

    // Match a character in the set [ABab] greedily any number of times.
    //{
        charloop_starting_pos = pos;

        int iteration = slice.IndexOfAnyExcept("ABab");
        if (iteration < 0)
        {
            iteration = slice.Length;
        }

        slice = slice.Slice(iteration);
        pos += iteration;

        charloop_ending_pos = pos;
        goto CharLoopEnd;

        CharLoopBacktrack:

        if (Utilities.s_hasTimeout)
        {
            base.CheckTimeout();
        }

        if (charloop_starting_pos >= charloop_ending_pos ||
            (charloop_ending_pos = inputSpan.Slice(charloop_starting_pos, charloop_ending_pos - charloop_starting_pos).LastIndexOfAny("BCbc")) < 0)
        {
            return false; // The input didn't match.
        }
        charloop_ending_pos += charloop_starting_pos;
        pos = charloop_ending_pos;
        slice = inputSpan.Slice(pos);

        CharLoopEnd:
    //}

    // Advance the next matching position.
    if (base.runtextpos < pos)
    {
        base.runtextpos = pos;
    }

    // Match a character in the set [BCbc].
    if (slice.IsEmpty || ((uint)((slice[0] | 0x20) - 'b') > (uint)('c' - 'b')))
    {
        goto CharLoopBacktrack;
    }

    // The input matched.
    pos++;
    base.runtextpos = pos;
    base.Capture(0, matchStart, pos);
    return true;
}

コード内のバックトラッキングの構造を確認できます。CharLoopBacktrack ラベルはバックトラック先に対して出力され、goto は正規表現の後続の部分が失敗したときにその場所にジャンプするために使われます。

RegexCompiler とソース ジェネレーターを実装しているコードを見ると、同じような名前のメソッド、同じような呼び出し構造、さらには実装全体を通して同じようなコメントで構成され、非常によく似ています。 一方は IL でもう一方は C# ですが、ほとんどの部分は同じコードになります。 もちろん、C# コンパイラによって C# は IL に変換されるため、両方のケースの結果の IL はおそらく同じではないでしょう。 ソース ジェネレーターはさまざまなケースでそれに依存しており、C# コンパイラがさまざまな C# コンストラクトをさらに最適化するという事実を利用しています。 ソース ジェネレーターによって RegexCompiler より最適化された照合コードが生成される具体的な場合がいくつかあります。 たとえば、前の例の 1 つでは、ソース ジェネレーターによって、'a' に対する分岐と 'b' に対する別の分岐を含む switch ステートメントが出力されることがわかります。 C# コンパイラは switch ステートメントの最適化に非常に優れており、複数の戦略を自由に使ってそれを効率的に行うことができるため、ソース ジェネレーターは、RegexCompiler にはない特別な最適化を備えています。 代替の場合、ソース ジェネレーターはすべての分岐を調べて、すべての分岐が異なる開始文字で始まっていることが証明できる場合は、その最初の文字で switch ステートメントを出力し、その代替に対するバックトラッキング コードは出力しません。

そのもう少し複雑な例を次に示します。 代替はさらに詳しく分析されて、バックトラッキング エンジンによる最適化がさらに簡単になり、ソース生成コードがより単純になるような方法でリファクタリングできるかどうかが判断されます。 このような最適化の 1 つでは、分岐からの共通プレフィックスの抽出がサポートされており、順序付けが重要ではないようなアトミックな代替の場合、そのような抽出をさらに可能にするためのブランチの並べ替えがサポートされています。 Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday のような曜日パターンで、その影響を確認できます。この場合、次のような照合関数が生成されます。

private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
    int pos = base.runtextpos;
    int matchStart = pos;
    char ch;
    ReadOnlySpan<char> slice = inputSpan.Slice(pos);

    // Match with 6 alternative expressions, atomically.
    {
        int alternation_starting_pos = pos;

        // Branch 0
        {
            if ((uint)slice.Length < 6 ||
                !slice.StartsWith("monday", StringComparison.OrdinalIgnoreCase)) // Match the string "monday" (ordinal case-insensitive)
            {
                goto AlternationBranch;
            }

            pos += 6;
            slice = inputSpan.Slice(pos);
            goto AlternationMatch;

            AlternationBranch:
            pos = alternation_starting_pos;
            slice = inputSpan.Slice(pos);
        }

        // Branch 1
        {
            if ((uint)slice.Length < 7 ||
                !slice.StartsWith("tuesday", StringComparison.OrdinalIgnoreCase)) // Match the string "tuesday" (ordinal case-insensitive)
            {
                goto AlternationBranch1;
            }

            pos += 7;
            slice = inputSpan.Slice(pos);
            goto AlternationMatch;

            AlternationBranch1:
            pos = alternation_starting_pos;
            slice = inputSpan.Slice(pos);
        }

        // Branch 2
        {
            if ((uint)slice.Length < 9 ||
                !slice.StartsWith("wednesday", StringComparison.OrdinalIgnoreCase)) // Match the string "wednesday" (ordinal case-insensitive)
            {
                goto AlternationBranch2;
            }

            pos += 9;
            slice = inputSpan.Slice(pos);
            goto AlternationMatch;

            AlternationBranch2:
            pos = alternation_starting_pos;
            slice = inputSpan.Slice(pos);
        }

        // Branch 3
        {
            if ((uint)slice.Length < 8 ||
                !slice.StartsWith("thursday", StringComparison.OrdinalIgnoreCase)) // Match the string "thursday" (ordinal case-insensitive)
            {
                goto AlternationBranch3;
            }

            pos += 8;
            slice = inputSpan.Slice(pos);
            goto AlternationMatch;

            AlternationBranch3:
            pos = alternation_starting_pos;
            slice = inputSpan.Slice(pos);
        }

        // Branch 4
        {
            if ((uint)slice.Length < 6 ||
                !slice.StartsWith("fr", StringComparison.OrdinalIgnoreCase) || // Match the string "fr" (ordinal case-insensitive)
                ((((ch = slice[2]) | 0x20) != 'i') & (ch != 'İ')) || // Match a character in the set [Ii\u0130].
                !slice.Slice(3).StartsWith("day", StringComparison.OrdinalIgnoreCase)) // Match the string "day" (ordinal case-insensitive)
            {
                goto AlternationBranch4;
            }

            pos += 6;
            slice = inputSpan.Slice(pos);
            goto AlternationMatch;

            AlternationBranch4:
            pos = alternation_starting_pos;
            slice = inputSpan.Slice(pos);
        }

        // Branch 5
        {
            // Match a character in the set [Ss].
            if (slice.IsEmpty || ((slice[0] | 0x20) != 's'))
            {
                return false; // The input didn't match.
            }

            // Match with 2 alternative expressions, atomically.
            {
                if ((uint)slice.Length < 2)
                {
                    return false; // The input didn't match.
                }

                switch (slice[1])
                {
                    case 'A' or 'a':
                        if ((uint)slice.Length < 8 ||
                            !slice.Slice(2).StartsWith("turday", StringComparison.OrdinalIgnoreCase)) // Match the string "turday" (ordinal case-insensitive)
                        {
                            return false; // The input didn't match.
                        }

                        pos += 8;
                        slice = inputSpan.Slice(pos);
                        break;

                    case 'U' or 'u':
                        if ((uint)slice.Length < 6 ||
                            !slice.Slice(2).StartsWith("nday", StringComparison.OrdinalIgnoreCase)) // Match the string "nday" (ordinal case-insensitive)
                        {
                            return false; // The input didn't match.
                        }

                        pos += 6;
                        slice = inputSpan.Slice(pos);
                        break;

                    default:
                        return false; // The input didn't match.
                }
            }

        }

        AlternationMatch:;
    }

    // The input matched.
    base.runtextpos = pos;
    base.Capture(0, matchStart, pos);
    return true;
}
private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
    int pos = base.runtextpos;
    int matchStart = pos;
    ReadOnlySpan<char> slice = inputSpan.Slice(pos);

    // Match with 5 alternative expressions, atomically.
    {
        if (slice.IsEmpty)
        {
            return false; // The input didn't match.
        }

        switch (slice[0])
        {
            case 'M':
                // Match the string "onday".
                if (!slice.Slice(1).StartsWith("onday"))
                {
                    return false; // The input didn't match.
                }

                pos += 6;
                slice = inputSpan.Slice(pos);
                break;

            case 'T':
                // Match with 2 alternative expressions, atomically.
                {
                    if ((uint)slice.Length < 2)
                    {
                        return false; // The input didn't match.
                    }

                    switch (slice[1])
                    {
                        case 'u':
                            // Match the string "esday".
                            if (!slice.Slice(2).StartsWith("esday"))
                            {
                                return false; // The input didn't match.
                            }

                            pos += 7;
                            slice = inputSpan.Slice(pos);
                            break;

                        case 'h':
                            // Match the string "ursday".
                            if (!slice.Slice(2).StartsWith("ursday"))
                            {
                                return false; // The input didn't match.
                            }

                            pos += 8;
                            slice = inputSpan.Slice(pos);
                            break;

                        default:
                            return false; // The input didn't match.
                    }
                }

                break;

            case 'W':
                // Match the string "ednesday".
                if (!slice.Slice(1).StartsWith("ednesday"))
                {
                    return false; // The input didn't match.
                }

                pos += 9;
                slice = inputSpan.Slice(pos);
                break;

            case 'F':
                // Match the string "riday".
                if (!slice.Slice(1).StartsWith("riday"))
                {
                    return false; // The input didn't match.
                }

                pos += 6;
                slice = inputSpan.Slice(pos);
                break;

            case 'S':
                // Match with 2 alternative expressions, atomically.
                {
                    if ((uint)slice.Length < 2)
                    {
                        return false; // The input didn't match.
                    }

                    switch (slice[1])
                    {
                        case 'a':
                            // Match the string "turday".
                            if (!slice.Slice(2).StartsWith("turday"))
                            {
                                return false; // The input didn't match.
                            }

                            pos += 8;
                            slice = inputSpan.Slice(pos);
                            break;

                        case 'u':
                            // Match the string "nday".
                            if (!slice.Slice(2).StartsWith("nday"))
                            {
                                return false; // The input didn't match.
                            }

                            pos += 6;
                            slice = inputSpan.Slice(pos);
                            break;

                        default:
                            return false; // The input didn't match.
                    }
                }

                break;

            default:
                return false; // The input didn't match.
        }
    }

    // The input matched.
    base.runtextpos = pos;
    base.Capture(0, matchStart, pos);
    return true;
}

ThursdayTuesday の直後に並べ替えられていること、および Tuesday/Thursday のペアと Saturday/Sunday のペアに対して行われていることに注目してください。これにより、複数レベルの switch が作成されます。 極端に言えば、多くの異なる単語の長い代替を作成する場合、ソース ジェネレーターは、各文字を読み取り、単語の残りの部分を処理するための分岐に switch することで、トライ^1と論理的に等価なものを出力します。 これは単語を照合する非常に効率的な方法であり、ソース ジェネレーターがここで行っていることです。

同時に、ソース ジェネレーターには、IL に直接出力するときに存在しない他の問題があります。 コード例をいくつか見返すと、いくつかの中かっこが奇妙にコメントアウトされていることがわかります。それは間違いではありません。 ソース ジェネレーターは、それらの中かっこがコメントアウトされていない場合、バックトラッキングの構造がスコープの外部からそのスコープ内で定義されているラベルへのジャンプに依存することを認識しています。このようなラベルはそのような goto からは見えず、コードのコンパイルは失敗します。 したがって、ソース ジェネレーターでは、そのようなスコープが存在しないようにする必要があります。 場合によっては、ここで行われたように単にスコープがコメントアウトされます。 それが不可能で、そうすることが問題になる場合は、スコープを必要とするコンストラクト (複数ステートメントの if ブロックなど) が回避される可能性があります。

ソース ジェネレーターは、1 つの例外を除き、すべての RegexCompiler ハンドルを処理します。 RegexOptions.IgnoreCase の処理と同様に、実装では、構築時にケーシング テーブルを使ってセットが生成され、IgnoreCase の前方参照の照合ではそのケーシング テーブルを参照する必要があります。 そのテーブルは System.Text.RegularExpressions.dll の内部であり、現在のところ、少なくとも、そのアセンブリの外部のコード (ソース ジェネレーターによって出力されたコードを含む) はそれにアクセスできません。 これにより、ソース ジェネレーターでの IgnoreCase 前方参照の処理が困難になり、サポートされていません。 これは、RegexCompiler によってサポートされていて、ソース ジェネレーターではサポートされていない 1 つのコンストラクトです。 これらのいずれかを含むパターン (まれなパターン) を使おうとすると、ソース ジェネレーターはカスタム実装を出力せず、代わりに通常の Regex インスタンスのキャッシュにフォールバックします。

まだキャッシュされているサポートされていない正規表現

また、RegexCompiler もソース ジェネレーターも、新しい RegexOptions.NonBacktracking はサポートしていません。 RegexOptions.Compiled | RegexOptions.NonBacktracking を指定した場合、Compiled フラグは単に無視され、ソース ジェネレーターに NonBacktracking を指定すると、同じように通常の Regex インスタンスのキャッシュにフォールバックします。

いつ使用するか

一般的なガイダンスとして、ソース ジェネレーターを使用できる場合は、それを使用します。 現在、C# でコンパイル時に引数がわかっている Regex を使っていて、特に既に RegexOptions.Compiled を使っている場合は (正規表現がスループットの向上のメリットを得るホット スポットとして識別されているため)、ソース ジェネレーターを使うことをお勧めします。 正規表現にソース ジェネレーターを使うと、次のような利点があります。

  • RegexOptions.Compiled のスループットに関するすべての利点。
  • 実行時にすべての正規表現の解析、分析、コンパイルを行う必要がないという起動時の利点。
  • 正規表現用に生成されたコードで事前コンパイルを使用するオプション。
  • 正規表現のデバッグ可能性と理解の向上。
  • RegexCompiler に関連するコードの大きな範囲をトリミングすることで、トリミングされたアプリのサイズを小さくすることが可能 (また、可能性としてはリフレクションの出力自体も)。

ソース ジェネレーターがカスタム実装を生成できない RegexOptions.NonBacktracking のようなオプションで使用しても、実装を記述するキャッシュと XML コメントが出力され、価値があります。 ソース ジェネレーターの主な欠点は、アセンブリに追加のコードを出力するため、サイズが大きくなる可能性があることです。 アプリ内の正規表現が多いほど、そしてそれが大きいほど、より多くのコードが出力されます。 状況によっては、RegexOptions.Compiled が必要ない場合があるのと同様に、ソース ジェネレーターも必要ない場合があります。 たとえば、まれにしか必要ない正規表現があり、スループットが重要でない場合は、その散発的な使用についてはインタープリターに依存する方が有益なことがあります。

重要

.NET 7 には、ソース ジェネレーターに変換できる Regex の使用を識別するアナライザーと、変換を行う修正ツールが含まれています:

RegexGenerator アナライザーと修正ツール

関連項目