다음을 통해 공유


보간된 문자열 개선

메모

이 문서는 기능 사양입니다. 사양은 기능의 디자인 문서 역할을 합니다. 여기에는 기능 디자인 및 개발 중에 필요한 정보와 함께 제안된 사양 변경 내용이 포함됩니다. 이러한 문서는 제안된 사양 변경이 완료되고 현재 ECMA 사양에 통합될 때까지 게시됩니다.

기능 사양과 완료된 구현 간에 약간의 불일치가 있을 수 있습니다. 이러한 차이는 언어 디자인 모임(LDM) 관련 노트에 기록됩니다.

사양문서에서 C# 언어 표준으로 스펙릿을 채택하는 과정에 대해 자세히 알아볼 수 있습니다.

챔피언 이슈: https://github.com/dotnet/csharplang/issues/4487

요약

효율적인 서식 지정과 사용을 가능하게 하며, 일반적인 string 시나리오뿐만 아니라 로깅 프레임워크와 같은 특수한 시나리오에서도 활용할 수 있도록, 불필요한 메모리 할당 없이 문자열 삽입 식을 생성하고 사용하는 새로운 패턴을 소개합니다.

동기

오늘날 문자열 보간은 주로 string.Format호출로 단순화됩니다. 범용이지만 다음과 같은 여러 가지 이유로 비효율적일 수 있습니다.

  1. 런타임에서 정확한 형식의 인수를 정확히 올바른 순서로 처리하는 string.Format 오버로드가 도입되지 않은 경우, 모든 구조체 인수를 박싱합니다.
    • 이 순서 지정은 런타임이 제네릭 버전의 메서드를 도입하는 것을 주저하는 이유입니다. 이는 매우 일반적인 메서드의 제네릭 인스턴스화가 결합적으로 폭발하기 때문에 발생합니다.
  2. 대부분의 경우 인수에 배열을 할당해야 합니다.
  3. 인스턴스가 필요하지 않은 경우 인스턴스를 인스턴스화하지 않도록 방지할 수 있는 기회가 없습니다. 예를 들어 로깅 프레임워크는 애플리케이션의 현재 로그 수준에 따라 필요하지 않을 수 있는 문자열이 실현되기 때문에 문자열 보간을 방지하는 것이 좋습니다.
  4. ref 구조체는 제네릭 형식 매개 변수로 허용되지 않으므로 오늘날에는 Span 또는 다른 ref 구조체 형식을 사용할 수 없습니다. 즉, 사용자가 중간 위치로 복사하지 않으려면 문자열의 서식을 수동으로 지정해야 합니다.

내부적으로 런타임에는 이러한 시나리오의 처음 2를 처리하는 데 도움이 되는 ValueStringBuilder 형식이 있습니다. stackalloc'd 버퍼를 작성기로 전달하고, 모든 부분으로 AppendFormat 반복적으로 호출한 다음, 최종 문자열을 가져옵니다. 결과 문자열이 스택 버퍼의 범위를 지나면 힙의 배열로 이동할 수 있습니다. 그러나 잘못된 사용으로 인해 임대된 배열이 이중 삭제될 수 있으므로 이 형식은 직접 노출하는 것이 위험합니다. 그러면 두 위치에서 임대 배열에 단독으로 액세스할 수 있다고 생각하므로 프로그램에서 정의되지 않은 모든 종류의 동작이 발생합니다. 이 제안은 보간된 문자열 리터럴만 작성하여 네이티브 C# 코드에서 이 형식을 안전하게 사용할 수 있는 방법을 만들고, 사용자가 작성하는 보간된 모든 문자열을 개선하는 동시에 작성 코드를 변경하지 않습니다. 또한 이 패턴을 확장하여 다른 메서드에 인수로 전달된 보간된 문자열이 메서드의 수신자가 정의한 처리기 패턴을 사용할 수 있도록 하여 프레임워크 로깅과 같이 필요하지 않은 문자열을 할당하지 않도록 하고 C# 사용자에게 친숙하고 편리한 보간 구문을 제공할 수 있도록 합니다.

상세 디자인

처리기 패턴

메서드에 인수로 전달된 보간된 문자열을 나타낼 수 있는 새 처리기 패턴을 소개합니다. 패턴의 간단한 영어는 다음과 같습니다.

interpolated_string_expression 메서드에 인수로 전달되는 경우 매개 변수의 형식을 확인합니다. 매개 변수 유형에 literalLengthformattedCount이라는 두 개의 int 매개 변수를 사용해 호출할 수 있는 생성자가 있고, 선택적으로 원래 매개 변수의 속성에 의해 지정된 추가 매개 변수를 취할 수 있으며, 선택적으로 후행 부울 출력 매개 변수가 존재하며, 원래 매개 변수의 유형에 보간된 문자열의 각 부분에 대해 호출할 수 있는 인스턴스 메서드 AppendLiteralAppendFormatted이 있다면, 우리는 이를 사용해 보간을 수행합니다. 이는 전통적인 string.Format(formatStr, args)호출 방식 대신 사용할 수 있습니다. 보다 구체적인 예제는 다음을 그리는 데 유용합니다.

// The handler that will actually "build" the interpolated string"
[InterpolatedStringHandler]
public ref struct TraceLoggerParamsInterpolatedStringHandler
{
    // Storage for the built-up string

    private bool _logLevelEnabled;

    public TraceLoggerParamsInterpolatedStringHandler(int literalLength, int formattedCount, Logger logger, out bool handlerIsValid)
    {
        if (!logger._logLevelEnabled)
        {
            handlerIsValid = false;
            return;
        }

        handlerIsValid = true;
        _logLevelEnabled = logger.EnabledLevel;
    }

    public void AppendLiteral(string s)
    {
        // Store and format part as required
    }

    public void AppendFormatted<T>(T t)
    {
        // Store and format part as required
    }
}

// The logger class. The user has an instance of this, accesses it via static state, or some other access
// mechanism
public class Logger
{
    // Initialization code omitted
    public LogLevel EnabledLevel;

    public void LogTrace([InterpolatedStringHandlerArguments("")]TraceLoggerParamsInterpolatedStringHandler handler)
    {
        // Impl of logging
    }
}

Logger logger = GetLogger(LogLevel.Info);

// Given the above definitions, usage looks like this:
var name = "Fred Silberberg";
logger.LogTrace($"{name} will never be printed because info is < trace!");

// This is converted to:
var name = "Fred Silberberg";
var receiverTemp = logger;
var handler = new TraceLoggerParamsInterpolatedStringHandler(literalLength: 47, formattedCount: 1, receiverTemp, out var handlerIsValid);
if (handlerIsValid)
{
    handler.AppendFormatted(name);
    handler.AppendLiteral(" will never be printed because info is < trace!");
}
receiverTemp.LogTrace(handler);

여기서는 TraceLoggerParamsInterpolatedStringHandler이(가) 올바른 매개 변수를 가진 생성자를 가지고 있기 때문에, 보간된 문자열에 해당 매개 변수로의 암시적 처리기 변환이 이루어지고, 이는 위에서 설명한 패턴으로 변환됩니다. 이에 필요한 사양은 약간 복잡하며 아래에서 확장됩니다.

이 제안의 나머지 부분에는 Append... 사용하여 둘 다 적용 가능한 경우 AppendLiteral 또는 AppendFormatted 중 하나를 참조합니다.

새 특성

컴파일러는 System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute인식합니다.

using System;
namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
    public sealed class InterpolatedStringHandlerAttribute : Attribute
    {
        public InterpolatedStringHandlerAttribute()
        {
        }
    }
}

이 특성은 형식이 유효한 보간된 문자열 처리기 형식인지 확인하기 위해 컴파일러에서 사용됩니다.

컴파일러는 System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute도 인식합니다.

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
    public sealed class InterpolatedStringHandlerArgumentAttribute : Attribute
    {
        public InterpolatedHandlerArgumentAttribute(string argument);
        public InterpolatedHandlerArgumentAttribute(params string[] arguments);

        public string[] Arguments { get; }
    }
}

이 특성은 매개 변수 위치에 사용되는 보간된 문자열 처리기 패턴을 낮추는 방법을 컴파일러에 알리기 위해 매개 변수에 사용됩니다.

보간된 문자열 핸들러 변환

T 형식은 System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute가 부여된 경우 applicable_interpolated_string_handler_type라고 합니다. interpolated_string_expression에서 T 암시적 interpolated_string_handler_conversion이 존재하거나, _interpolated_string_expression_s로만 완전히 구성되고 + 연산자만 사용하는 additive_expression이 있습니다.

이 스펙릿의 나머지 부분의 단순성을 위해, interpolated_string_expression은 간단한 interpolated_string_expression뿐만 아니라, + 연산자만을 사용하여 _interpolated_string_expression_s로만 전적으로 구성된 additive_expression도 참조합니다.

실제로 처리기 패턴을 사용하여 보간을 낮추려고 시도할 때 나중에 오류가 발생하든지 상관없이 이 변환은 항상 존재합니다. 이는 예측 가능하고 유용한 오류가 있고 보간된 문자열의 내용에 따라 런타임 동작이 변경되지 않도록 하기 위해 수행됩니다.

적용 가능한 함수 멤버 조정

다음과 같이 적용 가능한 함수 멤버 알고리즘(§12.6.4.2)의 표현을 조정합니다(새 하위 글머리 기호가 각 섹션에 굵게 추가됨).

함수 멤버는 인수 목록과 관련하여적용 가능한 함수 멤버라고 A.

  • A 각 인수는 해당 매개 변수(§12.6.2.2)에 설명된 대로 함수 멤버 선언의 매개 변수에 해당하며 인수가 없는 매개 변수는 선택적 매개 변수입니다.
  • A각 인수에 대해 인수의 매개 변수 전달 모드(예: 값, ref또는 out)는 해당 매개 변수의 매개 변수 전달 모드와 동일하며
    • 값 매개 변수 또는 매개 변수 배열의 경우 인수에서 해당 매개 변수 형식으로 암시적 변환(§10.2)이 존재하거나
    • 형식이 구조체인 ref 매개 변수의 경우, 인수에서 해당 매개 변수의 형식으로 암시적인 interpolated_string_handler_conversion이 있거나
    • ref 또는 out 매개 변수의 경우 인수 형식은 해당 매개 변수의 형식과 동일합니다. 결국 ref 또는 out 매개 변수는 전달된 인수의 별칭입니다.

매개 변수 배열을 포함하는 함수 멤버의 경우 위의 규칙에 따라 함수 멤버를 적용할 수 있는 경우 정규 형식적용할 수 있다고 합니다. 매개 변수 배열을 포함하는 함수 멤버를 일반 형식으로 적용할 수 없는 경우 함수 멤버는 확장된 형식적용할 수 있습니다.

  • 확장된 폼은 인수 목록의 인수 수가 총 매개 변수 수와 일치할 A 있도록 함수 멤버 선언의 매개 변수 배열을 매개 변수 배열의 요소 형식에 대한 0개 이상의 값 매개 변수로 바꿔서 생성됩니다. A 함수 멤버 선언의 고정 매개 변수 수보다 인수 수가 적으면 함수 멤버의 확장된 형식을 생성할 수 없으므로 적용할 수 없습니다.
  • 그렇지 않으면 인수의 매개 변수 전달 모드가 해당 매개 변수의 매개 변수 전달 모드와 동일할 A 각 인수에 대해 확장된 양식이 적용됩니다.
    • 고정 값 매개 변수 또는 확장에 의해 생성된 값 매개 변수의 경우 인수 형식에서 해당 매개 변수의 형식으로 암시적 변환(§10.2)이 존재합니다.
    • 형식이 구조체 형식인 ref 매개 변수에 대한 의 경우, 인수에서 해당 매개 변수의 형식으로의 암시적 Interpolated String Handler 변환이 존재하거나
    • ref 또는 out 매개 변수의 경우 인수 형식은 해당 매개 변수의 형식과 동일합니다.

중요 참고: 2개의 다른 동일한 오버로드가 있는 경우 applicable_interpolated_string_handler_type형식에만 다른 경우 이러한 오버로드는 모호한 것으로 간주됩니다. 또한, 명시적 캐스트를 통해 내용을 볼 수 없기 때문에 두 오버로드 모두 InterpolatedStringHandlerArguments을 사용하고, 이때 처리기 낮추기 패턴을 수동으로 수행하지 않으면 전혀 호출할 수 없게 되는 해결할 수 없는 상황이 발생할 수 있습니다. 선택하는 경우 이 문제를 해결하기 위해 더 나은 함수 멤버 알고리즘을 변경할 수 있지만 이 시나리오는 발생할 가능성이 낮으며 해결하기 위한 우선 순위가 아닙니다.

표현 조정에서의 더 나은 변환

식(§12.6.4.5) 섹션에서 더 나은 변환을 다음으로 변경합니다.

표현 E에서 형식 T1로 변환하는 암시적 변환 C1와 표현 E에서 형식 T2로 변환하는 암시적 변환 C2가 있을 때, C1은(는) C2에 비해 더 나은 변환입니다.

  1. E은 비상수 interpolated_string_expression이며, C1implicit_string_handler_conversion이며, T1applicable_interpolated_string_handler_type이며, C2implicit_string_handler_conversion이 아닙니다, 또는
  2. E T2 정확히 일치하지 않으며 다음 중 하나 이상이 성립합니다.
    • ET1과 정확히 일치합니다 (§12.6.4.5)
    • T1 T2(§12.6.4.6)보다 더 나은 변환 대상입니다.

즉, 문제의 보간된 문자열이 상수 식인지 여부에 따라 어쩌면 명확하지 않을 수 있는 오버로드 확인 규칙이 있습니다. 예를 들어:

void Log(string s) { ... }
void Log(TraceLoggerParamsInterpolatedStringHandler p) { ... }

Log($""); // Calls Log(string s), because $"" is a constant expression
Log($"{"test"}"); // Calls Log(string s), because $"{"test"}" is a constant expression
Log($"{1}"); // Calls Log(TraceLoggerParamsInterpolatedStringHandler p), because $"{1}" is not a constant expression

상수로 내보내기만 할 수 있고 오버헤드가 발생하지 않도록 하기 위해 도입되었으며, 상수일 수 없는 항목은 처리기 패턴을 사용합니다.

InterpolatedStringHandler 및 사용량

System.Runtime.CompilerServices에서 DefaultInterpolatedStringHandler의 새로운 유형을 소개합니다. C# 컴파일러에서 직접 사용하기 위한 ValueStringBuilder동일한 의미 체계가 많은 ref 구조체입니다. 이 구조체는 다음과 비슷합니다.

// API Proposal issue: https://github.com/dotnet/runtime/issues/50601
namespace System.Runtime.CompilerServices
{
    [InterpolatedStringHandler]
    public ref struct DefaultInterpolatedStringHandler
    {
        public DefaultInterpolatedStringHandler(int literalLength, int formattedCount);
        public string ToStringAndClear();

        public void AppendLiteral(string value);

        public void AppendFormatted<T>(T value);
        public void AppendFormatted<T>(T value, string? format);
        public void AppendFormatted<T>(T value, int alignment);
        public void AppendFormatted<T>(T value, int alignment, string? format);

        public void AppendFormatted(ReadOnlySpan<char> value);
        public void AppendFormatted(ReadOnlySpan<char> value, int alignment = 0, string? format = null);

        public void AppendFormatted(string? value);
        public void AppendFormatted(string? value, int alignment = 0, string? format = null);

        public void AppendFormatted(object? value, int alignment = 0, string? format = null);
    }
}

우리는 interpolated_string_expression 의미에 대한 규칙을 약간 변경합니다 (§12.8.3):

보간된 문자열의 형식이 string 있고 System.Runtime.CompilerServices.DefaultInterpolatedStringHandler 형식이 있고 현재 컨텍스트에서 해당 형식 사용을 지원하는 경우 처리기 패턴을 사용하여 문자열낮아집니다. 그런 다음 처리기 형식에서 ToStringAndClear() 호출하여 최종 string 값을 가져옵니다. 보간된 문자열의 형식이 System.IFormattable 또는 System.FormattableString 경우 [나머지는 변경되지 않음]

"현재 컨텍스트에서 해당 형식 사용을 지원합니다." 규칙은 의도적으로 모호하여 컴파일러가 이 패턴의 사용을 최적화할 수 있는 여유를 부여합니다. 처리기 형식은 ref 구조체 형식일 가능성이 높으며 일반적으로 비동기 메서드에서는 ref 구조체 형식이 허용되지 않습니다. 이 특정 사례에서는 보간 구멍 중 어느 것도 await 식을 포함하지 않는 경우, 처리기 형식이 추가적인 복잡한 분석 없이도 안전하게 사용될 수 있다는 것을 정적으로 확인할 수 있으므로, 보간된 문자열 식이 평가된 후 처리기가 삭제되면서, 컴파일러가 처리기를 사용할 수 있게 됩니다.

질문 열기:

컴파일러가 DefaultInterpolatedStringHandler을 인식하게 하고 string.Format 호출을 완전히 건너뛰도록 할까요? 우리는 string.Format을(를) 수동으로 호출할 때 굳이 사람들이 잘 알지 못하길 원하는 방법을 숨길 수 있습니다.

답변: 예.

질문 열기:

System.IFormattableSystem.FormattableString에 대한 처리기도 원하시나요?

답변: 아니요.

처리기 패턴 코드 생성

이 섹션에서 메서드 호출 확인은 §12.8.10.2나열된 단계를 참조합니다.

생성자 결정

applicable_interpolated_string_handler_typeTinterpolated_string_expressioni가 주어졌을 때, T에 대한 유효한 생성자를 위한 메서드 호출과 확인 작업은 다음과 같이 수행됩니다.

  1. 인스턴스 생성자에 대한 멤버 조회는 T수행됩니다. 결과 메서드 그룹을 M호출합니다.
  2. A 인수 목록은 다음과 같이 생성됩니다.
    1. 처음 두 인수는 각각 i의 리터럴 길이와 i보간 구성 요소 개수를 나타내는 정수 상수입니다.
    2. i이(가) 메서드 M1의 매개 변수 pi에 대한 인수로 사용되고, 매개 변수 piSystem.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute가 특성으로 부여된 경우, 해당 특성의 Arguments 배열에 있는 모든 이름 Argx에 대해 컴파일러가 동일한 이름을 가진 매개 변수 px과 일치시킵니다. 빈 문자열은 M1수신기와 일치합니다.
      • Argx M1매개 변수와 일치시킬 수 없거나 ArgxM1 수신자를 요청하고 M1 정적 메서드인 경우 오류가 발생하며 추가 단계가 수행되지 않습니다.
      • 그렇지 않으면 확인된 모든 px 형식이 Arguments 배열에서 지정한 순서대로 인수 목록에 추가됩니다. 각 pxM1에서 지정된 것과 동일한 ref 의미 체계로 전달됩니다.
    3. 최종 인수는 bool이며, 이것은 out 매개 변수로 전달됩니다.
  3. 기존 메서드 호출 확인은 메서드 그룹 M 및 인수 목록 A사용하여 수행됩니다. 메서드 호출 최종 유효성 검사를 위해 M 컨텍스트는 형식 T을(를) 통한 멤버 접근로 처리됩니다.
    • 최상의 단일 생성자 F가 발견되었을 때, 오버로드 해결의 결과는 F입니다.
    • 해당 생성자를 찾을 수 없는 경우 3단계를 다시 시도하여 A최종 bool 매개 변수를 제거합니다. 이 재시도에서 해당 멤버를 찾을 수 없으면 오류가 발생하며 추가 단계가 수행되지 않습니다.
    • 최상의 단일 메서드를 찾을 수 없는 경우 오버로드 확인 결과가 모호하고 오류가 발생하며 추가 단계가 수행되지 않습니다.
  4. F 대한 최종 유효성 검사가 수행됩니다.
    • A 요소가 i후 어휘적으로 발생한 경우 오류가 생성되고 추가 단계가 수행되지 않습니다.
    • AF의 수신자를 요청할 경우, 그리고 F라는 멤버 초기화 블록에서 초기화 목표로 사용되는 인덱서라면, 오류가 보고되고 추가적인 단계는 진행되지 않습니다.

참고: 여기서의 해결 방법은 의도적으로 Argx 요소에 대한 다른 인수로 전달된 실제 식을 사용하지 . 변환 후 형식만 고려합니다. 이렇게 하면 우리가 이중 변환 문제를 겪지 않으며, 람다가 M1로 전달될 때 한 대리자 형식에 바인딩되고 M로 전달될 때 다른 대리자 형식에 바인딩되는 예기치 않은 경우를 방지할 수 있습니다.

주의: 중첩 멤버 이니셜라이저의 평가 순서 때문에 우리가 멤버 이니셜라이저로 인덱서를 사용할 때 발생하는 오류를 보고합니다. 다음 코드 조각을 고려합니다.


var x1 = new C1 { C2 = { [GetString()] = { A = 2, B = 4 } } };

/* Lowering:
__c1 = new C1();
string argTemp = GetString();
__c1.C2[argTemp][1] = 2;
__c1.C2[argTemp][3] = 4;

Prints:
GetString
get_C2
get_C2
*/

string GetString()
{
    Console.WriteLine("GetString");
    return "";
}

class C1
{
    private C2 c2 = new C2();
    public C2 C2 { get { Console.WriteLine("get_C2"); return c2; } set { } }
}

class C2
{
    public C3 this[string s]
    {
        get => new C3();
        set { }
    }
}

class C3
{
    public int A
    {
        get => 0;
        set { }
    }
    public int B
    {
        get => 0;
        set { }
    }
}

__c1.C2[] 인수는 인덱서의 수신기를 전에 평가됩니다. 이 시나리오에 적합한 최적화를 마련할 수 있습니다. 즉, __c1.C2에 대한 임시 공간을 생성한 후 이를 두 인덱서 호출에 걸쳐 공유하거나, 첫 번째 인덱서 호출에만 사용하고 두 호출에서 이 인수를 공유하는 방법이 있습니다. 그러나 우리는 이러한 최적화가 우리가 병리적인 시나리오라고 생각하는 것에 대해 혼란을 줄 수 있다고 판단합니다. 따라서 시나리오를 완전히 금지했습니다.

열린 질문:

Create대신 생성자를 사용하는 경우 패턴을 약간 좁히는 대신 런타임 codegen을 개선합니다.

답변: 지금은 생성자로 제한합니다. 시나리오가 발생하는 경우 나중에 일반 Create 메서드를 추가하는 방법을 다시 확인할 수 있습니다.

Append... 메서드 오버로드 해결

applicable_interpolated_string_handler_typeTinterpolated_string_expressioni가 주어진 경우, T에서 유효한 Append... 메서드 집합에 대한 오버로드 해석은 다음과 같이 수행됩니다.

  1. i해석된 정규 문자열 문자 구성 요소가 있는 경우:
    1. AppendLiteral 이름의 T 멤버 조회가 수행됩니다. 결과 메서드 그룹을 Ml호출합니다.
    2. 인수 목록 Alstring형식의 값 매개 변수 하나로 생성됩니다.
    3. 기존 메서드 호출 확인은 메서드 그룹 Ml 및 인수 목록 Al사용하여 수행됩니다. 메서드 호출 최종 유효성 검사를 위해 Ml 컨텍스트는 T인스턴스를 통해 member_access 처리됩니다.
      • 최상의 단일 메서드 Fi 발견되고 오류가 생성되지 않은 경우 메서드 호출 확인의 결과는 Fi.
      • 그렇지 않으면 오류가 보고됩니다.
  2. 보간ix 구성 요소의 i마다:
    1. T에서 이름이 AppendFormatted인 멤버 조회가 수행됩니다. 결과 메서드 그룹을 Mf호출합니다.
    2. Af 인수 목록이 생성됩니다.
      1. 첫 번째 매개 변수는 값으로 전달되는 expressionix입니다.
      2. ix의 constant_expression 구성 요소를 직접 포함하는 경우, 이름이 alignment로 지정된 정수 값 매개 변수가 추가됩니다.
      3. ix 바로 뒤에 interpolation_format있는 경우, 이름이 format으로 지정된 문자열 값 매개 변수가 추가됩니다.
    3. 기존 메서드 호출 확인은 메서드 그룹 Mf 및 인수 목록 Af사용하여 수행됩니다. 메서드 호출 최종 유효성 검사를 위해 Mf 컨텍스트는 T인스턴스를 통해 member_access 처리됩니다.
      • 최상의 단일 메서드 Fi가 발견될 경우, 메서드 호출 결정 결과는 Fi입니다.
      • 그렇지 않으면 오류가 보고됩니다.
  3. 마지막으로 1단계와 2단계에서 검색된 모든 Fi 대해 최종 유효성 검사가 수행됩니다.
    • Fibool을 값으로 반환하지 않거나 void가 아니면 오류가 보고됩니다.
    • 모든 Fi 동일한 형식을 반환하지 않으면 오류가 보고됩니다.

이러한 규칙은 Append... 호출에 대한 확장 메서드를 허용하지 않습니다. 선택하는 경우 사용하도록 설정할 수 있지만 이는 GetEnumerator 확장 메서드가 되도록 허용하지만 Current 또는 MoveNext()허용하지 않는 열거자 패턴과 유사합니다.

이러한 규칙은 CallerLineNumber 또는 CallerArgumentExpression(언어에서 지원되는 경우) 작업하는 Append... 호출에 대한 기본 매개 변수를 허용하기 .

일부 처리기는 보간된 구성 요소와 기본 문자열의 일부인 구성 요소 간의 차이를 이해할 수 있기를 원하기 때문에 기본 요소와 보간 구멍에 대한 별도의 오버로드 조회 규칙이 있습니다.

열린 질문

구조화된 로깅과 같은 일부 시나리오에서는 보간 요소의 이름을 제공할 수 있어야 합니다. 예를 들어 오늘 로깅 호출은 Log("{name} bought {itemCount} items", name, items.Count);처럼 보일 수 있습니다. {} 내의 이름은 출력이 일관되고 균일한지 확인하는 데 도움이 되는 로거에 대한 중요한 구조 정보를 제공합니다. 보간 구멍의 :format 구성 요소를 다시 사용할 수 있는 경우도 있지만, 많은 로거가 이미 형식 지정자를 이해하고 있으며 이 정보를 기반으로 출력 서식 지정에 대한 기존 동작을 갖고 있습니다. 이러한 명명된 지정자를 배치하는 데 사용할 수 있는 몇 가지 구문이 있나요?

C# 10에 지원이 도입되는 경우, 일부 경우에서는 CallerArgumentExpression을 사용할 수 있습니다. 그러나 메서드/속성을 호출하는 경우 충분하지 않을 수 있습니다.

답변:

직교 언어 기능에서 탐색할 수 있는 템플릿 문자열에는 몇 가지 흥미로운 부분이 있지만 여기서 특정 구문은 튜플 사용과 같은 솔루션에 비해 많은 이점이 있다고 생각하지 않습니다. $"{("StructuredCategory", myExpression)}".

변환 수행

유효한 생성자 FcAppend... 메서드가 Faapplicable_interpolated_string_handler_typeTinterpolated_string_expressioni 확인되면 다음과 같이 i 대해 낮추기가 수행됩니다.

  1. i 전에 어휘적으로 발생하는 Fc 인수는 어휘 순서로 평가되고 임시 변수에 저장됩니다. 어휘 순서를 유지하기 위해 i 더 큰 식 e일부로 발생한 경우 i 전에 발생한 e 구성 요소도 어휘 순서로 다시 평가됩니다.
  2. Fc 보간된 문자열 리터럴 구성 요소의 길이, 보간 구멍 수, 이전에 평가한 인수 및 bool out 인수(Fc 마지막 매개 변수로 확인된 경우)로 호출됩니다. 결과는 ib임시 값에 저장됩니다.
    1. 리터럴 구성 요소의 길이는 모든 open_brace_escape_sequence을 단일 {로, 모든 close_brace_escape_sequence을 단일 }로 바꾼 후 계산됩니다.
  3. Fc이(가) bool out 인수로 끝나는 경우, 해당 bool 값에 대한 검사가 생성됩니다. true이면 Fa 메서드가 호출됩니다. 그렇지 않으면 호출되지 않습니다.
  4. Fa모든 FaxFax 현재 리터럴 구성 요소 또는 보간 식을 사용하여 ib 호출됩니다. Fax bool반환하는 경우 결과는 논리적으로 모든 이전 Fax 호출과 함께 생성됩니다.
    1. FaxAppendLiteral을 호출하는 경우, 리터럴 구성 요소는 모든 open_brace_escape_sequence를 단일 {로, 모든 close_brace_escape_sequence를 단일 }로 대체하여 이스케이프가 제거됩니다.
  5. 변환 결과는 ib.

다시 말하지만, Fc에 전달된 인수와 e에 전달된 인수는 동일한 임시 변수입니다. 변환은 Fc가 요구하는 형식으로 변환하기 위해 임시에서 발생할 수 있지만, 예를 들어, 람다는 Fce사이에서 다른 대리자 형식에 바인딩될 수 없습니다.

열린 질문

이렇게 낮추면 잘못된 반환 Append... 호출 후 보간된 문자열의 후속 부분이 평가되지 않습니다. 이는 특히 형식 구멍이 부작용을 일으키면 매우 혼란스러울 수 있습니다. 대신 먼저 모든 형식 구멍을 평가한 다음, 결과와 함께 Append... 반복적으로 호출하여 false를 반환하는 경우 중지할 수 있습니다. 이렇게 하면 모든 식이 예상대로 평가되지만 필요한 만큼 메서드를 적게 호출합니다. 일부 고급 사례에서는 부분 평가가 바람직할 수 있지만 일반적인 경우에는 직관적이지 않을 수 있습니다.

모든 형식 구멍을 항상 평가하려는 경우 또 다른 대안은 API의 Append... 버전을 제거하고 Format 호출을 반복하는 것입니다. 처리기는 인수를 삭제하고 이 버전에 대해 즉시 반환해야 하는지 여부를 추적할 수 있습니다.

답변: 구멍에 대한 조건부 평가를 수행합니다.

열린 질문

처분 가능한 처리기 유형을 정리하고, Dispose가 호출되도록 try/finally 문으로 호출을 감싸야 합니까? 예를 들어 bcl의 보간된 문자열 처리기에는 내부에 임대 배열이 있을 수 있으며, 보간 구멍 중 하나가 평가 중에 예외를 throw하는 경우 삭제되지 않은 경우 임대된 배열이 유출될 수 있습니다.

답변: 아니요. 처리기는 지역(예: MyHandler handler = $"{MyCode()};)에 할당할 수 있으며 이러한 처리기의 수명은 불분명합니다. 수명이 분명하고 열거자에 대해 사용자 정의 로컬이 만들어지지 않는 foreach 열거자와는 다릅니다.

nullable 참조 형식에 미치는 영향

구현의 복잡성을 최소화하기 위해 메서드 또는 인덱서에 대한 인수로 사용되는 보간된 문자열 처리기 생성자에 대해 nullable 분석을 수행하는 방법에 대한 몇 가지 제한 사항이 있습니다. 특히 생성자에서 원래 컨텍스트의 매개 변수 또는 인수의 원래 슬롯으로 정보를 다시 전달하지 않으며, 생성자 매개 변수 형식을 사용하여 포함하는 메서드의 형식 매개 변수에 대한 제네릭 형식 유추를 알리지 않습니다. 영향을 미칠 수 있는 위치의 예는 다음과 같습니다.

string s = "";
C c = new C();
c.M(s, $"", c.ToString(), s.ToString()); // No warnings on c.ToString() or s.ToString(), as the `MaybeNull` does not flow back.

public class C
{
    public void M(string s1, [InterpolatedStringHandlerArgument("", "s1")] CustomHandler c1, string s2, string s3) { }
}

[InterpolatedStringHandler]
public partial struct CustomHandler
{
    public CustomHandler(int literalLength, int formattedCount, [MaybeNull] C c, [MaybeNull] string s) : this()
    {
    }
}
string? s = null;
M(s, $""); // Infers `string` for `T` because of the `T?` parameter, not `string?`, as flow analysis does not consider the unannotated `T` parameter of the constructor

void M<T>(T? t, [InterpolatedStringHandlerArgument("s1")] CustomHandler<T> c) { }

[InterpolatedStringHandler]
public partial struct CustomHandler<T>
{
    public CustomHandler(int literalLength, int formattedCount, T t) : this()
    {
    }
}

기타 고려 사항

string 형식도 처리기로 변환할 수 있도록 허용

형식 작성자의 작업을 단순화하기 위해, 우리는 string 형식의 식이 applicable_interpolated_string_handler_types으로 암시적으로 변환될 수 있도록 고려할 수 있습니다. 현재 제안된 대로 작성자는 해당 처리기 유형과 일반 string 형식 모두에서 오버로드해야 하므로 사용자가 차이를 이해할 필요가 없습니다. string 식은 expression.Length 길이가 미리 채워지고 채울 구멍이 0개인 보간으로 간주될 수 있으므로, 이는 짜증나고 명확하지 않은 오버헤드일 수 있습니다.

이렇게 하면 새 API가 string-accepting 오버로드를 공개하지 않고도 처리기만 제공할 수 있습니다. 그러나 식에서 더 나은 변환을 위해 변경해야 하는 필요성을 해결할 수는 없으므로 작동하는 동안 불필요한 오버헤드가 발생할 수 있습니다.

답변:

이로 인해 혼란스러울 수 있으며 사용자 지정 처리기 형식에 대한 쉬운 해결 방법이 있습니다. 문자열에서 사용자 정의 변환을 추가합니다.

힙 없는 문자열에 대한 범위 통합

오늘날 존재하는 ValueStringBuilder에는 2개의 생성자가 있습니다. 하나는 개수를 받아 즉시 힙에 할당하는 것이고, 다른 하나는 Span<char>을 사용합니다. 이 Span<char> 일반적으로 런타임 코드베이스의 고정 크기이며 평균 약 250개 요소입니다. 해당 형식을 진정으로 대체하려면, 단순히 개수 버전만을 고려하는 것에 그치지 않고 Span<char>을 사용하는 GetInterpolatedString 메서드도 인식할 수 있는 확장을 검토해야 합니다. 그러나 우리가 여기서 해결해야 할 잠재적인 난해한 사례들이 몇 가지 있습니다.

  • 우리는 뜨거운 루프에서 반복적으로 stackalloc을 하지 않으려고 합니다. 이 기능을 확장한다면, 루프 반복 간에 stackalloc 범위를 공유하려고 할 것입니다. 우리는 이것이 안전하다는 것을 알고 있습니다. 왜냐하면 Span<T>은 힙에 저장할 수 없는 ref 구조체이므로, 사용자가 해당 Span에 대한 참조를 추출하려면 매우 교활해야 합니다(예를 들어, 그러한 핸들러를 수락하는 메서드를 만든 다음, 핸들러에서 Span를 고의적으로 검색하여 이를 호출자에게 반환하는 것과 같은 경우). 그러나 미리 할당하면 다음과 같은 다른 질문이 생성됩니다.
    • 우리가 stackalloc을 열심히 사용해야 할까요? 루프가 입력되지 않거나 공간이 필요하기 전에 종료되면 어떻게 될까요?
    • 우리가 stackalloc을 열심히 하지 않는다면, 그 결과로 모든 루프에 숨겨진 분기를 도입하게 되는 건가요? 대부분의 루프는 이에 대해 신경 쓰지 않을 수 있지만 비용을 지불하지 않으려는 일부 타이트 루프에 영향을 줄 수 있습니다.
  • 일부 문자열은 매우 클 수 있으며 stackalloc 적절한 양은 런타임 요인을 비롯한 여러 요인에 따라 달라집니다. C# 컴파일러 및 사양에서 이를 미리 결정하지 않기를 원하므로 https://github.com/dotnet/runtime/issues/25423 확인하고 이러한 경우 컴파일러에서 호출할 API를 추가하려고 합니다. 또한 이전 루프의 포인트에 더 많은 장단점을 추가합니다. 이 경우, 큰 배열을 힙에 여러 번 할당하는 것을 피하고, 필요하기 전에는 할당하지 않도록 합니다.

답변:

이는 C# 10의 범위를 벗어났습니다. 더 일반적인 params Span<T> 기능을 볼 때 일반적으로 이를 살펴볼 수 있습니다.

시험 버전이 아닌 API

간단히 하기 위해 이 사양은 현재 Append... 메서드를 인식할 것을 제안하며, 항상 성공하는 항목(예: InterpolatedStringHandler)은 항상 메서드에서 true를 반환합니다. 이는 오류가 발생하거나 로깅 사례와 같이 불필요한 경우 사용자가 서식을 중지하려고 하지만 표준 보간된 문자열 사용 시 불필요한 분기가 많이 발생할 수 있는 부분 서식 지정 시나리오를 지원하기 위해 수행되었습니다. Append... 메서드가 없는 경우 FormatX 메서드만 사용하는 부록을 고려할 수 있지만, Append... 호출과 FormatX 호출이 혼합되어 있는 경우 어떻게 해야 하는지에 대한 질문이 표시됩니다.

답변:

시도하지 않는 버전의 API를 원합니다. 제안이 이를 반영하도록 업데이트되었습니다.

처리기에 이전 인수 전달

현재 제안에 대칭이 부족합니다. 확장 메서드를 축소된 형식으로 호출하면 일반 형식으로 확장 메서드를 호출하는 것과는 다른 의미 체계가 생성됩니다. 이것은 감소 된 형태가 단지 설탕인 언어의 대부분의 다른 위치와 다릅니다. 메서드를 바인딩할 때 인식할 프레임워크에 특정 매개 변수를 처리기의 생성자에 전달해야 한다는 것을 컴파일러에 알리는 특성을 추가할 것을 제안합니다. 사용량은 다음과 같습니다.

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
    public sealed class InterpolatedStringHandlerArgumentAttribute : Attribute
    {
        public InterpolatedStringHandlerArgumentAttribute(string argument);
        public InterpolatedStringHandlerArgumentAttribute(params string[] arguments);

        public string[] Arguments { get; }
    }
}

이 사용법은 다음과 같습니다.

namespace System
{
    public sealed class String
    {
        public static string Format(IFormatProvider? provider, [InterpolatedStringHandlerArgument("provider")] ref DefaultInterpolatedStringHandler handler);
        …
    }
}

namespace System.Runtime.CompilerServices
{
    public ref struct DefaultInterpolatedStringHandler
    {
        public DefaultInterpolatedStringHandler(int baseLength, int holeCount, IFormatProvider? provider); // additional factory
        …
    }
}

var formatted = string.Format(CultureInfo.InvariantCulture, $"{X} = {Y}");

// Is lowered to

var tmp1 = CultureInfo.InvariantCulture;
var handler = new DefaultInterpolatedStringHandler(3, 2, tmp1);
handler.AppendFormatted(X);
handler.AppendLiteral(" = ");
handler.AppendFormatted(Y);
var formatted = string.Format(tmp1, handler);

답변해야 하는 질문:

  1. 일반적으로 이 패턴을 좋아합니까?
  2. 이러한 인수를 처리기 매개 변수 이후에서 가져오는 것을 허용하시겠습니까? BCL 내 일부 기존 패턴(Utf8Formatter)은 서식이 지정될 값을 서식 필요 요소보다 먼저에 배치합니다. 이러한 패턴에 가장 잘 맞도록 허용하고 싶지만, 순서에서 벗어난 평가가 괜찮은지 결정해야 합니다.

답변:

우리는 이것을 지원하고 싶습니다. 사양이 이를 반영하도록 업데이트되었습니다. 인수는 호출 사이트에서 어휘 순서로 지정해야 하며, 보간된 문자열 리터럴 후에 create 메서드에 필요한 인수를 지정하면 오류가 발생합니다.

보간 구멍의 await 사용

$"{await A()}"가 오늘날 유효한 식이므로, await을 사용하여 보간 구멍을 합리화해야 합니다. 다음과 같은 몇 가지 규칙을 사용하여 이 문제를 해결할 수 있습니다.

  1. 보간된 문자열이 string, IFormattable, 또는 FormattableString로 사용되는 경우, 보간 구멍에 await이 있으면 이전 스타일 포맷터를 사용합니다.
  2. 보간된 문자열이 implicit_string_handler_conversion의 대상이 되고 applicable_interpolated_string_handler_typeref struct인 경우, await는 형식 구멍에서 사용할 수 없습니다.

근본적으로, ref struct를 힙에 저장할 필요가 없다는 보장이 있는 한, 비동기 메서드에서 ref 구조체를 사용할 수 있습니다. 보간 위치에서 await을 금지하면 이것이 가능해야 합니다.

또는 보간된 문자열을 위한 프레임워크 핸들러를 포함하여, 모든 핸들러 유형을 ref가 아닌 구조체로 만들 수도 있습니다. 그러나 이는 우리가 언젠가 전혀 스크래치 공간을 할당할 필요가 없는 Span 버전을 인식하지 못하게 할 것입니다.

답변:

보간된 문자열 처리기를 다른 정수 형식과 동일하게 처리하겠습니다. 즉, 처리기 형식이 ref 구조체이고 현재 컨텍스트에서 ref 구조체의 사용을 허용하지 않는 경우, 여기서 처리기를 사용하는 것은 허용되지 않습니다. 문자열로 사용되는 문자열 리터럴을 낮추는 것과 관련된 사양은 컴파일러가 적절하다고 판단되는 규칙을 결정할 수 있도록 의도적으로 모호하지만 사용자 지정 처리기 형식의 경우 나머지 언어와 동일한 규칙을 따라야 합니다.

처리기를 ref 매개변수로 사용하기

일부 처리기는 ref 매개 변수(in 또는 ref)로 전달하려고 할 수 있습니다. 허용해야 하나요? 그렇다면 ref 처리기는 어떤 모습일까요? ref $""는 혼란스럽습니다. 실제로 문자열을 참조로 전달하는 것이 아니라, 참조를 통해 생성된 처리기를 참조로 전달하고 있으며, 이는 비동기 메서드와 유사한 잠재적인 문제가 발생할 수 있습니다.

답변:

우리는 이것을 지원하고 싶습니다. 사양이 이를 반영하도록 업데이트되었습니다. 규칙은 값 형식의 확장 메서드에 적용되는 것과 동일한 규칙을 반영해야 합니다.

이진 식 및 문자열 변환을 통해 삽입된 문자열

이 제안은 보간된 문자열의 컨텍스트를 중요하게 만들기 때문에 컴파일러가 보간된 문자열로 구성된 이진 식 또는 캐스트가 적용되는 보간된 문자열을 오버로드 확인을 위해 보간된 문자열 리터럴로 처리하도록 허용하려고 합니다. 예를 들어 다음 시나리오를 수행합니다.

struct Handler1
{
    public Handler1(int literalLength, int formattedCount, C c) => ...;
    // AppendX... methods as necessary
}
struct Handler2
{
    public Handler2(int literalLength, int formattedCount, C c) => ...;
    // AppendX... methods as necessary
}

class C
{
    void M(Handler1 handler) => ...;
    void M(Handler2 handler) => ...;
}

c.M($"{X}"); // Ambiguous between the M overloads

이는 모호함을 야기하며, 해결을 위해서는 Handler1 또는 Handler2 중 하나로 캐스팅이 필요합니다. 그러나 해당 캐스트를 만들 때 메서드 수신기의 컨텍스트에 대한 정보를 잃을 수 있습니다. 즉, c정보를 채울 수 없기 때문에 캐스트가 실패하게 됩니다. 문자열을 이진으로 연결할 때도 유사한 문제가 발생합니다. 사용자는 리터럴을 줄바꿈 없이 여러 줄에 걸쳐 서식화하여 표현하고자 할 수 있지만, 그렇게 될 경우 더 이상 처리기 형식으로 변환 가능한 보간된 문자열 리터럴이 되지 않으므로 불가능하게 됩니다.

이러한 경우를 해결하기 위해 다음과 같이 변경합니다.

  • additive_expression가 오직 interpolated_string_expressions로 구성되고 + 연산자만 사용하는 경우, 변환 및 오버로드 해결을 위한 목적에서 interpolated_string_literal로 간주됩니다. 보간된 마지막 문자열은 모든 개별 interpolated_string_expression 구성 요소를 왼쪽에서 오른쪽으로 논리적으로 연결하여 생성됩니다.
  • 피연산자가 삽입 문자열 표현식인 경우의 연산자 as을 포함하는 형 변환 표현식 또는 관계 표현식은 변환 및 오버로드 확인의 목적상 삽입 문자열 표현식으로 간주됩니다.

열린 질문:

이 작업을 수행하시겠습니까? 예를 들어 System.FormattableString대해서는 이 작업을 수행하지 않지만 다른 줄로 나눌 수 있는 반면 컨텍스트에 따라 달라지므로 다른 줄로 나눌 수 없습니다. FormattableStringIFormattable대한 오버로드 해결 문제도 없습니다.

답변:

가산 식에 대한 유효한 사용 사례라고 생각하지만, 현재는 캐스트 버전이 충분히 매력적이지 않다고 생각합니다. 필요한 경우 나중에 추가할 수 있습니다. 이 결정을 반영하도록 사양이 업데이트되었습니다.

기타 사용 사례

이 패턴을 사용하는 제안된 처리기 API의 예제는 https://github.com/dotnet/runtime/issues/50635 참조하세요.