다음을 통해 공유


자습서: 사용자 지정 문자열 보간 처리기 작성

이 자습서에서는 다음 방법을 알아봅니다.

  • 문자열 보간 처리기 패턴 구현
  • 문자열 보간 작업에서 수신기와 상호 작용합니다.
  • 문자열 보간 처리기에 인수 추가
  • 문자열 보간을 위한 새로운 라이브러리 기능을 알아보세요

필수 구성 요소

.NET을 실행하려면 컴퓨터를 설정해야 합니다. C# 컴파일러는 Visual Studio 2022 또는 .NET SDK사용할 수 있습니다.

이 자습서에서는 Visual Studio 또는 .NET CLI를 포함하여 C# 및 .NET에 익숙하다고 가정합니다.

사용자 지정 보간된 문자열 처리기를 작성할 수 있습니다 . 보간된 문자열 처리기는 보간된 문자열에서 자리 표시자 식을 처리하는 타입입니다. 사용자 지정 처리기가 없을 경우, 자리 표시자는 String.Format와 비슷하게 처리됩니다. 각 자리 표시자는 텍스트로 서식이 지정되고 구성 요소가 연결되어 결과 문자열을 형성합니다.

결과 문자열에 대한 정보를 사용하는 모든 시나리오에 대한 처리기를 작성할 수 있습니다. 사용되나요? 형식에 어떤 제약 조건이 있나요? 몇 가지 예는 다음과 같습니다.

  • 결과 문자열 중 그 어느 것도 80자 같은 제한을 넘지 않도록 요구할 수 있습니다. 보간된 문자열을 처리하여 고정 길이 버퍼를 채우고, 그 길이에 도달하면 처리를 중지할 수 있습니다.
  • 테이블 형식이 있을 수 있으며, 각 자리 표시자는 고정된 길이를 가져야 합니다. 사용자 지정 처리기는 모든 클라이언트 코드를 준수하도록 강제하는 대신 이를 적용할 수 있습니다.

이 자습서에서는 핵심 성능 시나리오 중 하나인 로깅 라이브러리에 대한 문자열 보간 처리기를 만듭니다. 구성된 로그 수준에 따라 로그 메시지를 생성하는 작업이 필요하지 않습니다. 로깅이 해제된 경우 보간된 문자열 식에서 문자열을 생성하는 작업은 필요하지 않습니다. 메시지는 인쇄되지 않으므로 문자열 연결을 건너뛸 수 있습니다. 또한, 자리 표시자에서 사용되는 표현은 스택 추적 생성을 포함하여 수행할 필요가 없습니다.

보간된 문자열 처리기는 서식이 지정된 문자열을 사용할지 여부를 결정하고 필요한 경우에만 필요한 작업을 수행할 수 있습니다.

초기 구현

다양한 수준을 지원하는 기본 Logger 클래스에서 시작해 보겠습니다.

public enum LogLevel
{
    Off,
    Critical,
    Error,
    Warning,
    Information,
    Trace
}

public class Logger
{
    public LogLevel EnabledLevel { get; init; } = LogLevel.Error;

    public void LogMessage(LogLevel level, string msg)
    {
        if (EnabledLevel < level) return;
        Console.WriteLine(msg);
    }
}

Logger 여섯 가지 수준을 지원합니다. 메시지가 로그 수준 필터를 전달하지 않으면 출력이 없습니다. 로거에 대한 공용 API는 (완전히 형식이 지정된) 문자열을 메시지로 허용합니다. 문자열을 만드는 모든 작업이 이미 완료되었습니다.

처리기 패턴 구현

이 단계는 현재 동작을 다시 만드는 보간된 문자열 처리기 빌드하는 것입니다. 보간된 문자열 처리기는 다음과 같은 특징을 가져야 하는 유형입니다.

  • 형식에 적용된 System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute.
  • literalLengthformattedCount두 개의 int 매개 변수가 있는 생성자입니다. (더 많은 매개 변수가 허용됨).
  • 서명이 있는 공용 AppendLiteral 메서드: public void AppendLiteral(string s).
  • 시그니처가 있는 제네릭 공용 AppendFormatted 메서드: public void AppendFormatted<T>(T t).

내부적으로 작성기에서 형식이 지정된 문자열을 만들고 클라이언트에서 해당 문자열을 검색할 수 있는 멤버를 제공합니다. 다음 코드는 이러한 요구 사항을 충족하는 LogInterpolatedStringHandler 형식을 보여 줍니다.

[InterpolatedStringHandler]
public ref struct LogInterpolatedStringHandler
{
    // Storage for the built-up string
    StringBuilder builder;

    public LogInterpolatedStringHandler(int literalLength, int formattedCount)
    {
        builder = new StringBuilder(literalLength);
        Console.WriteLine($"\tliteral length: {literalLength}, formattedCount: {formattedCount}");
    }

    public void AppendLiteral(string s)
    {
        Console.WriteLine($"\tAppendLiteral called: {{{s}}}");
        
        builder.Append(s);
        Console.WriteLine($"\tAppended the literal string");
    }

    public void AppendFormatted<T>(T t)
    {
        Console.WriteLine($"\tAppendFormatted called: {{{t}}} is of type {typeof(T)}");

        builder.Append(t?.ToString());
        Console.WriteLine($"\tAppended the formatted object");
    }

    internal string GetFormattedText() => builder.ToString();
}

이제 LogMessageLogger 클래스의 오버로드를 추가하여 새로운 보간 문자열 처리기를 시도할 수 있습니다.

public void LogMessage(LogLevel level, LogInterpolatedStringHandler builder)
{
    if (EnabledLevel < level) return;
    Console.WriteLine(builder.GetFormattedText());
}

원래 LogMessage 메서드를 제거할 필요가 없습니다. 컴파일러는 인수가 보간된 문자열 식인 경우 string 매개 변수가 있는 메서드보다 보간된 처리기 매개 변수가 있는 메서드를 선호합니다.

다음 코드를 주 프로그램으로 사용하여 새 처리기가 호출되는지 확인할 수 있습니다.

var logger = new Logger() { EnabledLevel = LogLevel.Warning };
var time = DateTime.Now;

logger.LogMessage(LogLevel.Error, $"Error Level. CurrentTime: {time}. This is an error. It will be printed.");
logger.LogMessage(LogLevel.Trace, $"Trace Level. CurrentTime: {time}. This won't be printed.");
logger.LogMessage(LogLevel.Warning, "Warning Level. This warning is a string, not an interpolated string expression.");

애플리케이션을 실행하면 다음 텍스트와 유사한 출력이 생성됩니다.

        literal length: 65, formattedCount: 1
        AppendLiteral called: {Error Level. CurrentTime: }
        Appended the literal string
        AppendFormatted called: {10/20/2021 12:19:10 PM} is of type System.DateTime
        Appended the formatted object
        AppendLiteral called: {. This is an error. It will be printed.}
        Appended the literal string
Error Level. CurrentTime: 10/20/2021 12:19:10 PM. This is an error. It will be printed.
        literal length: 50, formattedCount: 1
        AppendLiteral called: {Trace Level. CurrentTime: }
        Appended the literal string
        AppendFormatted called: {10/20/2021 12:19:10 PM} is of type System.DateTime
        Appended the formatted object
        AppendLiteral called: {. This won't be printed.}
        Appended the literal string
Warning Level. This warning is a string, not an interpolated string expression.

출력을 추적하면 컴파일러가 처리기를 호출하고 문자열을 빌드하는 코드를 추가하는 방법을 확인할 수 있습니다.

  • 컴파일러는 처리기를 생성하는 호출을 추가하여 형식 문자열에 있는 리터럴 텍스트의 총 길이와 자리 표시자 수를 전달합니다.
  • 컴파일러는 리터럴 문자열의 각 섹션과 각 자리 표시자에 대해 AppendLiteralAppendFormatted 호출을 추가합니다.
  • 컴파일러는 인수로 CoreInterpolatedStringHandler 사용하여 LogMessage 메서드를 호출합니다.

마지막으로, 마지막 경고문은 인터폴레이티드 문자열 처리기를 호출하지 않습니다. 인수가 string일 경우, 호출은 문자열 매개 변수를 사용하는 다른 오버로드를 자동으로 호출합니다.

중요하다

이 섹션의 Logger 버전은 ref struct입니다. ref struct는 스택에 저장되어야 하므로 메모리 할당을 최소화합니다. 그러나 일반적으로 ref struct 형식은 인터페이스를 구현할 수 없습니다. 이로 인해 단위 테스트 프레임워크와 구현에 ref struct 유형을 사용하는 모킹 유형에 대한 호환성 문제가 발생할 수 있습니다.

처리기에 더 많은 기능 추가

보간된 문자열 처리기의 이전 버전은 패턴을 구현합니다. 자리 표시자 식을 모두 처리하지 않으려면 처리기에 추가 정보가 필요합니다. 이 섹션에서는 생성된 문자열이 로그에 기록되지 않을 때 덜 작동하도록 처리기를 개선합니다. System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute 사용하여 공용 API에 대한 매개 변수와 처리기의 생성자에 대한 매개 변수 간의 매핑을 지정합니다. 이를 통해 처리기에 보간된 문자열을 평가해야 하는지 여부를 결정하는 데 필요한 정보가 제공됩니다.

처리기의 변경 사항부터 시작해 보겠습니다. 먼저 처리기가 활성화되어 있는지 확인할 필드를 추가합니다. 생성자에 두 개의 매개 변수를 추가합니다. 하나는 이 메시지의 로그 수준을 지정하고 다른 하나는 로그 개체에 대한 참조입니다.

private readonly bool enabled;

public LogInterpolatedStringHandler(int literalLength, int formattedCount, Logger logger, LogLevel logLevel)
{
    enabled = logger.EnabledLevel >= logLevel;
    builder = new StringBuilder(literalLength);
    Console.WriteLine($"\tliteral length: {literalLength}, formattedCount: {formattedCount}");
}

다음으로, 필드를 사용하여 마지막 문자열이 사용될 때 처리기가 리터럴 또는 서식이 지정된 개체만 추가하도록 하십시오.

public void AppendLiteral(string s)
{
    Console.WriteLine($"\tAppendLiteral called: {{{s}}}");
    if (!enabled) return;

    builder.Append(s);
    Console.WriteLine($"\tAppended the literal string");
}

public void AppendFormatted<T>(T t)
{
    Console.WriteLine($"\tAppendFormatted called: {{{t}}} is of type {typeof(T)}");
    if (!enabled) return;

    builder.Append(t?.ToString());
    Console.WriteLine($"\tAppended the formatted object");
}

다음으로 컴파일러가 처리기의 생성자에 추가 매개 변수를 전달하도록 LogMessage 선언을 업데이트해야 합니다. 처리기 인수의 System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute을 사용하여 작업이 처리됩니다.

public void LogMessage(LogLevel level, [InterpolatedStringHandlerArgument("", "level")] LogInterpolatedStringHandler builder)
{
    if (EnabledLevel < level) return;
    Console.WriteLine(builder.GetFormattedText());
}

이 속성은 필수 literalLengthformattedCount 매개 변수를 따른 뒤에 오는 매개 변수에 매핑되는 LogMessage의 인수 목록을 지정합니다. 빈 문자열("")은 수신기를 지정합니다. 컴파일러는 핸들러의 생성자에 대한 다음 인자의 값으로 this이 나타내는 Logger 객체의 값을 대입합니다. 컴파일러는 다음 인수에 level 값을 대입합니다. 작성하는 모든 처리기에 대해 임의의 수의 인수를 제공할 수 있습니다. 추가하는 인수는 문자열 인수입니다.

동일한 테스트 코드를 사용하여 이 버전을 실행할 수 있습니다. 이번에는 다음과 같은 결과가 표시됩니다.

        literal length: 65, formattedCount: 1
        AppendLiteral called: {Error Level. CurrentTime: }
        Appended the literal string
        AppendFormatted called: {10/20/2021 12:19:10 PM} is of type System.DateTime
        Appended the formatted object
        AppendLiteral called: {. This is an error. It will be printed.}
        Appended the literal string
Error Level. CurrentTime: 10/20/2021 12:19:10 PM. This is an error. It will be printed.
        literal length: 50, formattedCount: 1
        AppendLiteral called: {Trace Level. CurrentTime: }
        AppendFormatted called: {10/20/2021 12:19:10 PM} is of type System.DateTime
        AppendLiteral called: {. This won't be printed.}
Warning Level. This warning is a string, not an interpolated string expression.

AppendLiteralAppendFormat 메서드가 호출되고 있지만 아무 작업도 수행하지 않는 것을 볼 수 있습니다. 처리기는 최종 문자열이 필요하지 않다고 판단하여 그것을 빌드하지 않습니다. 아직 몇 가지 개선 사항이 있습니다.

먼저 인수를 System.IFormattable구현하는 형식으로 제한하는 AppendFormatted 오버로드를 추가할 수 있습니다. 이 오버로드를 사용하면 호출자가 자리 표시자에 형식 문자열을 추가할 수 있습니다. 이 변경을 수행하는 동안 다른 AppendFormattedAppendLiteral 메서드의 반환 형식을 voidbool 변경해 보겠습니다(이러한 메서드 중 다른 반환 형식이 있으면 컴파일 오류가 발생합니다). 이러한 변경으로 이(가)을(를) 단축할 수 있게 합니다. 메서드는 보간된 문자열 식의 처리를 중지해야 함을 나타내기 위해 false을(를) 반환합니다. true을 반환하면 계속 진행해야 합니다. 이 예제에서는 결과 문자열이 필요하지 않은 경우 처리를 중지하는 데 이 함수를 사용하고 있습니다. 쇼트 서킷은 보다 세분화된 작업을 지원합니다. 고정 길이 버퍼를 지원하기 위해 특정 길이에 도달하면 식 처리를 중지할 수 있습니다. 또는 일부 조건은 나머지 요소가 필요하지 않음을 나타낼 수 있습니다.

public void AppendFormatted<T>(T t, string format) where T : IFormattable
{
    Console.WriteLine($"\tAppendFormatted (IFormattable version) called: {t} with format {{{format}}} is of type {typeof(T)},");

    builder.Append(t?.ToString(format, null));
    Console.WriteLine($"\tAppended the formatted object");
}

이 추가 기능을 통해 보간된 문자열 식에서 형식 문자열을 지정할 수 있습니다.

var time = DateTime.Now;

logger.LogMessage(LogLevel.Error, $"Error Level. CurrentTime: {time}. The time doesn't use formatting.");
logger.LogMessage(LogLevel.Error, $"Error Level. CurrentTime: {time:t}. This is an error. It will be printed.");
logger.LogMessage(LogLevel.Trace, $"Trace Level. CurrentTime: {time:t}. This won't be printed.");

첫 번째 메시지의 :t는 현재 시간의 "짧은 시간 형식"을 지정합니다. 이전 예제에서는 처리기에 대해 만들 수 있는 AppendFormatted 메서드에 대한 오버로드 중 하나를 보여 줍니다. 서식이 지정된 개체에 대한 제네릭 인수를 지정할 필요가 없습니다. 만드는 형식을 문자열로 변환하는 더 효율적인 방법이 있을 수 있습니다. 제네릭 인수 대신 해당 형식을 사용하는 AppendFormatted 오버로드를 작성할 수 있습니다. 컴파일러는 최상의 오버로드를 선택합니다. 런타임은 이 기술을 사용하여 System.Span<T> 문자열 출력으로 변환합니다. 정수 매개 변수를 추가하여 IFormattable포함하거나 생략하여 출력의 맞춤 지정할 수 있습니다. .NET 6과 함께 제공되는 System.Runtime.CompilerServices.DefaultInterpolatedStringHandler 다양한 용도에 대한 9개의 AppendFormatted 오버로드를 포함합니다. 용도에 맞게 처리기를 빌드하는 동안 참조로 사용할 수 있습니다.

이제 샘플을 실행하면 Trace 메시지에 대해서, 첫 번째 AppendLiteral만 호출되는 것을 볼 수 있습니다.

        literal length: 60, formattedCount: 1
        AppendLiteral called: Error Level. CurrentTime:
        Appended the literal string
        AppendFormatted called: 10/20/2021 12:18:29 PM is of type System.DateTime
        Appended the formatted object
        AppendLiteral called: . The time doesn't use formatting.
        Appended the literal string
Error Level. CurrentTime: 10/20/2021 12:18:29 PM. The time doesn't use formatting.
        literal length: 65, formattedCount: 1
        AppendLiteral called: Error Level. CurrentTime:
        Appended the literal string
        AppendFormatted (IFormattable version) called: 10/20/2021 12:18:29 PM with format {t} is of type System.DateTime,
        Appended the formatted object
        AppendLiteral called: . This is an error. It will be printed.
        Appended the literal string
Error Level. CurrentTime: 12:18 PM. This is an error. It will be printed.
        literal length: 50, formattedCount: 1
        AppendLiteral called: Trace Level. CurrentTime:
Warning Level. This warning is a string, not an interpolated string expression.

처리기의 생성자를 마지막으로 업데이트하여 효율성을 향상시킬 수 있습니다. 처리기는 최종 out bool 매개 변수를 추가할 수 있습니다. 해당 매개 변수를 false 설정하면 보간된 문자열 식을 처리하기 위해 처리기를 전혀 호출해서는 안 됩니다.

public LogInterpolatedStringHandler(int literalLength, int formattedCount, Logger logger, LogLevel level, out bool isEnabled)
{
    isEnabled = logger.EnabledLevel >= level;
    Console.WriteLine($"\tliteral length: {literalLength}, formattedCount: {formattedCount}");
    builder = isEnabled ? new StringBuilder(literalLength) : default!;
}

이 변경은 enabled 필드를 제거할 수 있음을 의미합니다. 그런 다음 AppendLiteralAppendFormatted의 반환 형식을 void로 변경할 수 있습니다. 이제 샘플을 실행하면 다음 출력이 표시됩니다.

        literal length: 60, formattedCount: 1
        AppendLiteral called: Error Level. CurrentTime:
        Appended the literal string
        AppendFormatted called: 10/20/2021 12:19:10 PM is of type System.DateTime
        Appended the formatted object
        AppendLiteral called: . The time doesn't use formatting.
        Appended the literal string
Error Level. CurrentTime: 10/20/2021 12:19:10 PM. The time doesn't use formatting.
        literal length: 65, formattedCount: 1
        AppendLiteral called: Error Level. CurrentTime:
        Appended the literal string
        AppendFormatted (IFormattable version) called: 10/20/2021 12:19:10 PM with format {t} is of type System.DateTime,
        Appended the formatted object
        AppendLiteral called: . This is an error. It will be printed.
        Appended the literal string
Error Level. CurrentTime: 12:19 PM. This is an error. It will be printed.
        literal length: 50, formattedCount: 1
Warning Level. This warning is a string, not an interpolated string expression.

LogLevel.Trace이 지정되었을 때 생성자의 출력만 나타납니다. 처리기는 사용할 수 없다는 표시가 되어 있어 Append 메서드 중 어떤 것도 호출되지 않았습니다.

이 예는 로깅 라이브러리를 사용할 때 보간 문자열 처리기가 갖는 중요한 역할을 잘 보여줍니다. 플레이스홀더에서 부작용이 발생하지 않을 수도 있습니다. 주 프로그램에 다음 코드를 추가하고 이 동작이 작동하는지 확인합니다.

int index = 0;
int numberOfIncrements = 0;
for (var level = LogLevel.Critical; level <= LogLevel.Trace; level++)
{
    Console.WriteLine(level);
    logger.LogMessage(level, $"{level}: Increment index a few times {index++}, {index++}, {index++}, {index++}, {index++}");
    numberOfIncrements += 5;
}
Console.WriteLine($"Value of index {index}, value of numberOfIncrements: {numberOfIncrements}");

루프를 반복할 때마다 index 변수가 5배씩 증가되는 것을 볼 수 있습니다. 자리 표시자는 Critical, Error, 및 Warning 수준에 대해서만 평가되며, InformationTrace수준은 평가되지 않으므로, index의 최종 값이 예상과 일치하지 않습니다.

Critical
Critical: Increment index a few times 0, 1, 2, 3, 4
Error
Error: Increment index a few times 5, 6, 7, 8, 9
Warning
Warning: Increment index a few times 10, 11, 12, 13, 14
Information
Trace
Value of index 15, value of numberOfIncrements: 25

보간된 문자열 처리기는 보간된 문자열 식이 문자열로 변환되는 방법을 보다 정교하게 제어합니다. .NET 런타임 팀은 이 기능을 사용하여 여러 영역에서 성능을 향상했습니다. 고유한 라이브러리에서 동일한 기능을 사용할 수 있습니다. 추가로 탐색하려면 System.Runtime.CompilerServices.DefaultInterpolatedStringHandler를 살펴보세요. 여기서 빌드한 것보다 더 완전한 구현을 제공합니다. Append 메서드에 사용할 수 있는 훨씬 더 많은 오버로드가 표시됩니다.