다음을 통해 공유


.NET 정규식 소스 생성기

정규식은 개발자가 검색되는 패턴을 표현할 수 있는 문자열로, 검색된 문자열에서 텍스트를 검색하고 결과를 하위 집합으로 추출하는 일반적인 방법입니다. .NET에서 System.Text.RegularExpressions 네임스페이스는 Regex 인스턴스 및 정적 메서드를 정의하고 사용자 정의 패턴과 일치시키는 데 사용됩니다. 이 문서에서는 소스 생성을 사용하여 Regex 인스턴스를 생성해 성능을 최적화하는 방법을 알아봅니다.

참고 항목

가능하다면 RegexOptions.Compiled 옵션을 사용하여 정규식을 컴파일하는 대신 소스에서 생성된 정규식을 사용하세요. 원본 생성을 통해 앱을 더 빠르게 시작하고, 더 빠르게 실행하고, 더 간편하게 다듬을 수 있습니다. 원본 생성이 가능한 시기를 알아보려면 사용 시기를 참조하세요.

컴파일된 정규식

new Regex("somepattern")를 작성하면 몇 가지 프로세스가 발생합니다. 지정된 패턴이 구문 분석됩니다. 이는 패턴의 유효성을 보장하고 구문 분석된 정규식을 나타내는 내부 트리로 변환하기 위한 것입니다. 그런 다음 트리가 다양한 방법으로 최적화되어 패턴을 보다 효율적으로 실행할 수 있는 기능적으로 동등한 변형으로 변환합니다. 트리는 정규식 인터프리터 엔진에 일치 방법에 대한 명령을 제공하는 일련의 opcode 및 피연산자로 해석될 수 있는 양식으로 작성됩니다. 일치가 수행되면 인터프리터는 입력 텍스트에 대해 이러한 명령을 처리합니다. 인터프리터는 새 Regex 인스턴스를 인스턴스화하거나 Regex에서 정적 메서드 중 하나를 호출할 때 사용하는 기본 엔진입니다.

RegexOptions.Compiled를 지정하면 동일한 구문 시간 작업이 모두 수행됩니다. 결과 명령은 반사 방출 기반 컴파일러에 의해 몇 가지 DynamicMethod 개체에 기록되는 IL 명령으로 추가로 변환됩니다. 일치가 수행되면 해당 DynamicMethod 메서드가 호출됩니다. 이 IL은 처리되는 정확한 패턴에 특화된 것을 제외하고 기본적으로 인터프리터가 수행하는 작업을 정확하게 수행합니다. 예를 들어, 패턴에 [ac]가 포함된 경우 인터프리터는 "이 집합 설명에 지정된 집합과 현재 위치의 입력 문자를 일치시킵니다"라는 opcode를 보게 됩니다. 반면 컴파일된 IL에는 "현재 위치의 입력 문자를 'a' 또는 'c'와 일치시킵니다"라고 효과적으로 말하는 코드가 포함됩니다. 이 특수 대/소문자 구분 및 패턴 지식을 기반으로 최적화를 수행하는 기능은 RegexOptions.Compiled 지정이 인터프리터보다 훨씬 빠르게 일치를 처리하는 주된 이유 중 일부입니다.

RegexOptions.Compiled에는 몇 가지 단점이 있습니다. 가장 큰 영향은 구성하는 데 비용이 많이 든다는 것입니다. 인터프리터와 동일한 비용은 물론이고 결과 RegexNode 트리 및 생성된 opcode/피연산자를 IL로 컴파일하기 위한 적지 않은 비용이 추가됩니다. 생성된 IL은 처음 사용할 때 JIT를 추가로 컴파일해야 하므로 시작 시 더 많은 비용이 발생합니다. RegexOptions.Compiled는 처음 사용할 때의 오버헤드와 이후의 모든 사용에 대한 오버헤드 간의 근본적인 절충을 나타냅니다. System.Reflection.Emit의 사용은 또한 특정 환경에서 RegexOptions.Compiled의 사용을 억제합니다. 일부 운영 체제는 동적으로 생성된 코드를 실행할 수 없으므로 이러한 시스템에서는 Compiled가 작동하지 않습니다.

원본 생성

.NET 7에는 새 RegexGenerator 소스 생성기가 도입되었습니다. 원본 생성기는 컴파일러에 연결되고 추가 소스 코드로 컴파일 단위를 확장하는 구성 요소입니다. .NET SDK(버전 7 이상)에는 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
    }
}

RegexOptions.Compiled 플래그는 원본 생성기에서 무시되므로 원본 생성 버전에서는 필요하지 않습니다.

생성된 AbcOrDefGeneratedRegex() 구현은 마찬가지로 싱글톤 Regex 인스턴스를 캐시하므로 코드를 사용하는 데 추가 캐싱이 필요하지 않습니다.

다음 이미지는 소스 생성 캐시 인스턴스, internal에서 소스 생성기가 방출하는 Regex 하위 클래스의 화면 캡처입니다.

캐시된 정규식 정적 필드

그러나 보다시피 new Regex(...)만 수행하는 것이 아닙니다. 대신 소스 생성기는 IL에서 RegexOptions.Compiled가 내보내는 것과 유사한 논리를 사용하여 C# 코드로 사용자 지정 Regex 파생 구현을 내보냅니다. RegexOptions.Compiled의 모든 처리량 성능 이점(실제로는 그 이상)과 Regex.CompileToAssembly의 시작 이점을 얻으면서도 CompileToAssembly의 복잡성은 없습니다. 내보내는 원본은 프로젝트의 일부이므로 쉽게 보고 디버그할 수도 있습니다.

소스 생성 Regex 코드를 통한 디버깅

Visual Studio에서 부분 메서드 선언을 마우스 오른쪽 단추로 클릭하고 정의로 이동을 선택합니다. 또는 솔루션 탐색기에서 프로젝트 노드를 선택한 다음 종속성>분석기>System.Text.RegularExpressions.Generator>System.Text.RegularExpressions.Generator.RegexGenerator>RegexGenerator.g.cs를 확장하여 이 정규식 생성기에서 생성된 C# 코드를 확인합니다.

중단점을 설정하고, 한 단계씩 실행할 수 있으며, 학습 도구로 사용하여 정규식 엔진이 입력에서 패턴을 처리하는 방법을 정확하게 이해할 수 있습니다. 생성기는 식의 내용과 사용되는 위치를 한눈에 파악할 수 있도록 XML(트리플 슬래시) 주석을 생성합니다.

정규식을 설명하는 생성된 XML 주석

소스 생성 파일 내부

.NET 7에서는 소스 생성기 및 RegexCompiler가 모두 거의 완전히 다시 작성되어 생성되는 코드의 구조가 근본적으로 달라졌습니다. 이 접근 방식은 모든 구문을 처리하도록 확장되었으며(한 가지 주의 사항), 새 접근 방식에 따라 여전히 RegexCompiler와 소스 생성기가 대부분 1:1로 매핑됩니다. abc|def 식의 주 함수 중 하나에 대한 소스 생성기 출력을 예로 들어보겠습니다.

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# 구문을 추가로 최적화한다는 장점 때문에 다양한 경우에 C# 컴파일러를 사용합니다. 소스 생성기가 RegexCompiler보다 최적화된 일치 코드를 생성하는 몇 가지 구체적인 경우가 있습니다. 예를 들어 이전 예제 중 하나에서 switch 문을 내보내는 소스 생성기가 있습니다. 'a'에 대한 분기 하나와 'b'에 대한 또 다른 분기가 있습니다. C# 컴파일러는 switch 문을 최적화하는 데 매우 능숙하여 여러 전략을 사용하여 처리할 수 있으므로 소스 생성기에는 RegexCompiler에 없는 특수 최적화가 있습니다. 변경의 경우 소스 생성기는 모든 분기를 살펴보고 모든 분기가 다른 시작 문자로 시작된다는 것을 증명할 수 있는 경우 첫 번째 문자에 대한 switch 문을 내보내고 해당 변경에 대한 역추적 코드를 출력하지 않습니다.

여기에 약간 더 복잡한 예가 있습니다. 역추적 엔진에서 보다 쉽게 최적화할 수 있고 소스 생성 코드가 더 간단해지는 방식으로 리팩터링할 수 있는지 여부를 확인하기 위해 변경을 더 집중적으로 분석합니다. 이러한 최적화 중 하나는 분기에서 공통 접두사를 추출하도록 지원하고 변경이 원자성이어서 순서가 중요하지 않은 경우 더 많은 추출을 허용하도록 분기를 다시 정렬합니다. 다음과 같은 일치 함수를 생성하는, 다음 요일 패턴 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 쌍 모두 여러 수준의 스위치로 구성되었습니다. 극단적인 경우 여러 단어의 긴 변경을 만드는 경우 소스 생성기는 trie^1의 논리적 등가를 내보내고 단어의 나머지 부분을 처리하기 위해 각 문자를 읽고 분기로 switching합니다. 이는 단어를 일치시킬 수 있는 매우 효율적인 방법이며 여기서 소스 생성기가 수행하는 작업입니다.

동시에 소스 생성기에는 IL로 직접 출력할 때는 존재하지 않는 다른 문제가 있습니다. 몇 가지 코드 예제를 다시 보면 일부 중괄호가 다소 이상하게 주석 처리된 것을 볼 수 있습니다. 이것은 실수가 아닙니다. 소스 생성기는 이러한 중괄호가 주석 처리되지 않은 경우 역추적 구조가 해당 범위 외부에서 해당 범위 내에 정의된 레이블로 이동하는 데 의존한다고 인식합니다. 이러한 레이블은 goto에 표시되지 않으며 코드가 컴파일되지 않습니다. 따라서 소스 생성기는 방해가 되는 범위가 없도록 해야 합니다. 경우에 따라 여기에서와 같이 범위를 주석 처리하기만 하면 됩니다. 이것이 가능하지 않은 다른 경우에는 문제가 될 수 있으면 범위가 필요한 구문(예: 다중 문 if 블록)을 사용하지 않습니다.

소스 생성기는 한 가지 예외를 제외하고 모든 RegexCompiler 핸들을 처리합니다. RegexOptions.IgnoreCase 처리와 마찬가지로 구현은 이제 대/소문자 테이블을 사용하여 생성 시 집합을 생성하고 IgnoreCase 역참조 일치가 해당 대/소문자 테이블을 참조하는 데 필요한 방법을 설명합니다. 해당 테이블은 System.Text.RegularExpressions.dll 내부에 있으며, 적어도 지금은 해당 어셈블리 외부의 코드(소스 생성기에서 내보낸 코드 포함)가 액세스할 수 없습니다. 따라서 IgnoreCase 역참조 처리는 소스 생성기에서 문제가 되며 지원되지 않습니다. 이것이 RegexCompiler는 지원하지만 소스 생성기는 지원하지 않는 한 구문입니다. 드물지만 이러한 패턴 중 하나를 사용하려고 하면 소스 생성기가 사용자 지정 구현을 내보내는 대신 일반 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 분석기 및 수정 도구

추가 정보