Поделиться через


Улучшенные интерполированные строки

Заметка

Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Она включает предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию ECMA.

Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Эти различия фиксируются в соответствующих заметках собрания по проектированию языка (LDM).

Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .

Проблема чемпиона: https://github.com/dotnet/csharplang/issues/4487

Сводка

Мы представляем новый шаблон для создания и использования интерполированных строковых выражений, чтобы обеспечить эффективное форматирование и использование как в общих string сценариях, так и в более специализированных сценариях, таких как фреймворки для ведения журнала, без ненужных выделений памяти из-за форматирования строки в фреймворке.

Мотивация

Сегодня интерполяция строк в основном сводится к вызову string.Format. Это, в то время как общее назначение, может быть неэффективным по ряду причин:

  1. Он выполняет упаковку всех аргументов типа struct, если только среда выполнения случайно не ввела перегрузку string.Format, которая принимает строго корректные типы аргументов и именно в указанном порядке.
    • Именно поэтому среда выполнения не стремится внедрять обобщённые версии метода, поскольку это приведет к комбинаторному взрыву обобщённых инстанциаций очень распространенного метода.
  2. В большинстве случаев он должен выделить массив для аргументов.
  3. Невозможно избежать создания экземпляра, если это не требуется. Например, фреймворки для логирования рекомендуют избегать интерполяции строк, поскольку это может привести к созданию строки, которая может быть не нужна, в зависимости от текущего уровня журнала приложения.
  4. В настоящее время он не может использовать Span или другие типы структур ссылок, потому что ссылочные структуры не допускаются в качестве параметров универсального типа, то есть если пользователь хочет избежать копирования в промежуточные расположения, ему приходится вручную форматировать строки.

Внутри среды выполнения есть тип, который называется ValueStringBuilder для решения первых 2 из этих сценариев. Они передают буфер, выделенный в стеке, строителю, неоднократно вызывая AppendFormat с каждой частью, а затем получают окончательную строку. Если результирующая строка выходит за пределы буфера стека, они могут перейти на массив в куче. Однако этот тип опасно использовать непосредственно, так как неправильное использование может привести к тому, что арендуемый массив будет дважды удалён, что затем вызовет различные непредсказуемые последствия в программе, поскольку два участка могут полагать, что обладают единственным доступом к арендуемому массиву. Это предложение создает способ безопасного использования этого типа из нативного кода C#, просто написав интерполированный строковый литерал, оставляя написанный код без изменений и улучшая каждую интерполированную строку, которую пользователь пишет. Он также расширяет этот шаблон, чтобы разрешить использовать интерполированные строки, передаваемые в качестве аргументов другим методам, с помощью шаблона обработчика, определяемого приёмником метода. Это позволит таким системам, как фреймворки ведения логов, избежать выделения строк, которые не понадобятся, и предоставить пользователям C# знакомый и удобный синтаксис интерполяции.

Подробный дизайн

Шаблон обработчика

Мы введем новый шаблон обработчика, который может представлять интерполированную строку, переданную в качестве аргумента методу. Простой английский шаблон выглядит следующим образом:

Когда interpolated_string_expression передается в качестве аргумента методу, мы рассмотрим тип параметра. Если тип параметра имеет конструктор, который можно вызвать с двумя параметрами int, literalLength и formattedCount, по желанию принимает дополнительные параметры, указанные атрибутом исходного параметра, по желанию может иметь выходной логический заключительный параметр, и тип исходного параметра имеет методы AppendLiteral и AppendFormatted, которые могут быть вызваны для каждой части интерполированной строки, то мы выполняем интерполяцию другим способом, вместо традиционного вызова 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 считается applicable_interpolated_string_handler_type, если он имеет атрибут System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute. Существует неявный interpolated_string_handler_conversion для T из interpolated_string_expressionили additive_expression, полностью состоящий из _interpolated_string_expression_s и использующий только операторы +.

Для простоты в остальной части этой спецификации interpolated_string_expression относится как к простому interpolated_string_expression, так и к additive_expression, полностью состоящему из _interpolated_string_expression_s и используя только операторы +.

Обратите внимание, что это преобразование всегда существует, независимо от того, будут ли более поздние ошибки при попытке снизить интерполяцию с помощью шаблона обработчика. Это делается для обеспечения прогнозируемых и полезных ошибок, а поведение среды выполнения не изменяется на основе содержимого интерполированной строки.

Применимые корректировки элементов функции

Мы корректируем формулировку применимого алгоритма члена функции (§12.6.4.2) следующим образом: (в каждый раздел добавляется новый подпункт, выделенный полужирным шрифтом):

Как говорят, член функции является применимым членом функции относительно списка аргументов A, если все из следующих значений имеют значение true:

  • Каждый аргумент в A соответствует параметру в объявлении члена функции, как описано в соответствующих параметрах (§12.6.2.2), а любой параметр, к которому аргумент не соответствует, является необязательным параметром.
  • Для каждого аргумента в Aрежим передачи аргумента (т. е. значение, refили out) идентичен режиму передачи соответствующего параметра, и
    • для параметра значения или массива параметров неявное преобразование (§10.2) существует из аргумента в тип соответствующего параметра или
    • для параметра ref, тип которого является структурным, существует неявное преобразование типа interpolated_string_handler_conversion из аргумента в тип соответствующего параметра, или
    • для параметра ref или out тип аргумента идентичен типу соответствующего параметра. В конце концов, параметр ref или out является псевдонимом переданного аргумента.

Для элемента функции, включающего массив параметров, если член функции применяется в приведенных выше правилах, он, как утверждается, применяется в его обычной форме. Если член функции, содержащий массив параметров, неприменимо в обычной форме, элемент функции может применяться в его развернутой форме:

  • Расширенная форма создается путем замены массива параметров в объявлении члена функции нулевыми или более параметрами значения типа элемента массива параметров, таким образом, что число аргументов в списке аргументов A соответствует общему числу параметров. Если A имеет меньше аргументов, чем число фиксированных параметров в объявлении члена функции, расширенная форма члена функции не может быть создана и поэтому неприменимо.
  • В противном случае развернутая форма применима, если для каждого аргумента в A режим передачи параметра аргумента идентичен режиму передачи параметров соответствующего параметра и
    • для параметра фиксированного значения или параметра значения, созданного расширением, неявное преобразование (§10.2) существует из типа аргумента в тип соответствующего параметра или
    • для параметра ref, тип которого — структура, неявное преобразование interpolated_string_handler_conversion существует из аргумента в тип соответствующего параметра или
    • для параметра ref или out тип аргумента идентичен типу соответствующего параметра.

Важно отметить: это означает, что при наличии 2 эквивалентных перегрузок, различающихся только по типу applicable_interpolated_string_handler_type, эти перегрузки будут считаться неоднозначными. Кроме того, поскольку мы не видим явных приведений, возможно, возникнет неразрешимый сценарий, когда обе применимые перегрузки используют InterpolatedStringHandlerArguments и становятся полностью невызываемыми без ручного выполнения шаблона снижения обработчика. Мы могли бы внести изменения в алгоритм поиска лучшего члена функции, чтобы решить эту проблему, если мы захотим, но этот сценарий вряд ли произойдет и не является приоритетной задачей.

Улучшенное преобразование посредством корректировки выражений

Мы изменим лучшее преобразование из выражения (§12.6.4.5) на следующее:

Учитывая неявное преобразование C1, которое преобразуется из выражения E в тип T1, а неявное преобразование C2, которое преобразуется из выражения E в тип T2, C1 — это лучшее преобразование, чем C2, если:

  1. E является неконстантным interpolated_string_expression, C1 является implicit_string_handler_conversion, T1 является applicable_interpolated_string_handler_type, а C2 не является implicit_string_handler_conversion, или
  2. E точно не соответствует T2 и хотя бы одному из следующих условий:
    • E точно соответствует T1 (§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

Это введено для того, чтобы вещи, которые можно просто выдавать как константы, не влекли за собой никаких затрат, а те, которые не могут быть постоянными, использовали шаблон обработчика.

ИнтерполированныйОбработчикСтрок и использование

Мы представляем новый тип в System.Runtime.CompilerServices: DefaultInterpolatedStringHandler. Это структура ссылок со многими из той же семантики, что и ValueStringBuilder, предназначенная для прямого использования компилятором C#. Эта структура будет выглядеть примерно так:

// 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 существует, а текущий контекст поддерживает использование этого типа, строкаснижается с помощью шаблона обработчика. Затем последнее значение string получается путем вызова ToStringAndClear() в типе обработчика.В противном случае, если тип интерполированной строки System.IFormattable или System.FormattableString [остальные не изменяются]

Правило "и текущий контекст поддерживает использование этого типа" намеренно расплывчато, чтобы предоставить компилятору возможность оптимизировать использование этого шаблона. Тип обработчика, скорее всего, будет типом структуры ref, и такие типы структур обычно не допускаются в асинхронных методах. В данном случае компилятору будет разрешено использовать обработчик, если ни одно из отверстий интерполяции не содержит выражение await, так как мы можем статически определить, что тип обработчика безопасно используется без дополнительного сложного анализа, так как обработчик будет удален после вычисления интерполированного строкового выражения.

Открыть вопрос:

Хотим ли мы вместо этого просто заставить компилятор узнать о DefaultInterpolatedStringHandler и полностью пропустить вызов string.Format? Это позволит нам скрыть метод, который мы не обязательно хотим ставить на виду у людей, когда они вручную вызывают string.Format.

Ответ: Да.

Открыть вопрос:

Хотим ли мы добавить обработчики для System.IFormattable и System.FormattableString?

Ответ: Нет.

Кодеген шаблона обработчика

В этом разделе разрешение вызова метода ссылается на шаги, перечисленные в §12.8.10.2.

Разрешение конструктора

Учитывая applicable_interpolated_string_handler_typeT и interpolated_string_expressioni, разрешение вызова метода и проверка допустимого конструктора на T выполняются следующим образом:

  1. Поиск членов для экземплярных конструкторов выполняется на T. Результирующая группа методов называется M.
  2. Список аргументов A создается следующим образом:
    1. Первые два аргумента представляют собой целые константы, представляющие литеральную длину i, а также количество компонентов интерполяции в iсоответственно.
    2. Если i используется в качестве аргумента для некоторого параметра pi в методе M1, и параметр pi отмечен атрибутом System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute, то для каждого имени Argx в массиве Arguments этого атрибута компилятор сопоставляет его с параметром px с тем же именем. Пустая строка соответствует приемнику M1.
      • Если какой-либо Argx не удается сопоставить с параметром M1или если Argx запрашивает приемник M1, а M1 является статическим методом, возникает ошибка, и дальнейшие шаги не предпринимаются.
      • В противном случае тип каждого разрешенного px добавляется в список аргументов в порядке, указанном массивом Arguments. Каждый px передается с такой же семантикой ref, как указано в M1.
    3. Последний аргумент — это bool, переданный в качестве параметра out.
  3. Традиционное разрешение вызова метода выполняется с помощью групп методов M и списка аргументов A. В целях окончательной проверки вызова метода контекст M рассматривается как member_access через тип T.
    • Если найден единственный лучший конструктор F, то результатом разрешения перегрузки является F.
    • Если применимые конструкторы не найдены, выполните шаг 3, удалив окончательный параметр bool из A. Если эта повторная попытка также не находит применимых элементов, возникает ошибка и дальнейшие действия не выполняются.
    • Если ни один лучший метод не найден, результат разрешения перегрузки неоднозначно, возникает ошибка, и дальнейшие шаги не выполняются.
  4. Выполняется окончательная проверка на F.
    • Если любой элемент A произошел лексически после i, возникает ошибка и дальнейшие действия не выполняются.
    • Если A запрашивает от получателя F, а F используется как индексатор в качестве initializer_target в member_initializer, то сообщается об ошибке и никакие дальнейшие действия не предпринимаются.

Примечание. Решение здесь намеренно не использовать фактические выражения, переданные в качестве других аргументов для 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используется конструктор, мы бы улучшили кодеген среды выполнения за счет сужения шаблона немного.

ответ: мы ограничимся конструкторами. Мы можем вернуться к добавлению универсального метода Create позже, если возникнет такая необходимость.

разрешение перегрузки метода Append...

Учитывая applicable_interpolated_string_handler_typeT и interpolated_string_expressioni, разрешение перегрузки для набора допустимых методов Append... в T выполняется следующим образом:

  1. Если в iесть компоненты интерполированного регулярного строкового символа:
    1. Выполняется поиск члена на T с именем AppendLiteral. Результирующая группа методов называется Ml.
    2. Список аргументов Al создается с одним параметром значения типа string.
    3. Традиционное разрешение вызова метода выполняется с помощью групп методов Ml и списка аргументов Al. В целях окончательной проверки вызова метода контекст Ml рассматривается как member_access через экземпляр T.
      • Если найден один наилучший метод Fi и ошибки не возникли, результат разрешения вызова метода Fi.
      • В противном случае сообщается об ошибке.
  2. Для каждого компонента интерполяции ixi:
    1. Выполняется поиск элемента с именем AppendFormatted на T. Результирующая группа методов называется Mf.
    2. Список аргументов Af построен:
      1. Первым параметром является expression из ix, передаваемый по значению.
      2. Если ix непосредственно содержит компонент constant_expression, добавляется целочисленный параметр значения с указанным именем alignment.
      3. Если за ix непосредственно следует interpolation_format, добавляется параметр строкового значения с указанным именем format.
    3. Традиционное разрешение вызова метода выполняется с помощью групп методов Mf и списка аргументов Af. В целях окончательной проверки вызова метода контекст Mf рассматривается как member_access через экземпляр T.
      • Если найден один лучший метод Fi, результат разрешения вызова метода Fi.
      • В противном случае сообщается об ошибке.
  3. Наконец, для каждого Fi, обнаруженного на шагах 1 и 2, выполняется окончательная проверка:
    • Если любая Fi не возвращает bool по значению или void, сообщается об ошибке.
    • Если все Fi не возвращают одинаковый тип, сообщается об ошибке.

Обратите внимание, что эти правила не разрешают методы расширения для вызовов Append.... Мы могли бы рассмотреть возможность включения этого варианта, но это аналогично шаблону перечислителя, где мы разрешаем GetEnumerator быть методом расширения, но не Current или MoveNext().

Эти правила разрешают параметры по умолчанию для вызовов Append..., которые будут работать с такими вещами, как CallerLineNumber или CallerArgumentExpression (если поддерживается языком).

У нас есть отдельные правила поиска перегрузки для базовых элементов и отверстий интерполяции, так как некоторые обработчики хотят иметь возможность понять разницу между компонентами, которые были интерполированы и компонентами, которые были частью базовой строки.

Открыть вопрос

Некоторые сценарии, такие как структурированное ведение журнала, хотят иметь возможность предоставлять имена для элементов интерполяции. Например, сегодня вызов ведения журнала может выглядеть как Log("{name} bought {itemCount} items", name, items.Count);. Имена внутри структуры {} предоставляют важные сведения о структуре для логгеров, которые помогают обеспечить согласованность и единообразие выходных данных. В некоторых случаях можно повторно использовать компонент :format интерполяционного отверстия, но многие логгеры уже понимают спецификаторы формата и обладают устоявшимся поведением для форматирования выходных данных на основе этой информации. Существует ли какой-либо синтаксис, который можно использовать для ввода этих именованных параметров?

Некоторые случаи могут обойтись CallerArgumentExpression, если поддержка будет включена в C# 10. В случаях, реализация которых вызывает метод или свойство, этого может быть недостаточно.

ответ:

Хотя существуют некоторые интересные части шаблонных строк, которые мы могли бы исследовать в функции ортогонального языка, мы не считаем, что конкретный синтаксис здесь имеет много преимуществ для таких решений, как использование кортежа: $"{("StructuredCategory", myExpression)}".

Выполнение преобразования

Учитывая applicable_interpolated_string_handler_typeT и интерполированное_строковое_выражениеi, для которого был определен допустимый конструктор Fc и методы Append..., Fa, понижение для i выполняется следующим образом:

  1. Любые аргументы для Fc, которые встречаются лексически до i, вычисляются и хранятся во временных переменных в лексическом порядке. Чтобы сохранить лексическое упорядочение, если i произошло в рамках более крупного выражения e, все компоненты e, которые произошли до i, будут оцениваться также в лексическом порядке.
  2. Fc вызывается с длиной интерполированных строковых литералов, числом отверстий для замещения , любыми ранее вычисленными аргументами и выходным аргументом bool (если Fc был выполнен с одним в качестве последнего параметра). Результат сохраняется во временном значении ib.
    1. Длина литеральных компонентов вычисляется после замены любого open_brace_escape_sequence одним {и любой close_brace_escape_sequence одним }.
  3. Если Fc закончился с аргументом типа out bool, создается проверка значения bool. Если значение true, методы в Fa будут вызываться. В противном случае они не будут вызываться.
  4. Для каждого Fax в FaFax вызывается на ib в подходящий момент с либо текущим литеральным компонентом, либо выражением интерполяции . Если Fax возвращает bool, результат логически соединяется с всеми предыдущими вызовами Fax.
    1. Если Fax является вызовом AppendLiteral, экранирование литерального компонента удаляется путем замены любого open_brace_escape_sequence одним {, и любой close_brace_escape_sequence одним }.
  5. Результат преобразования — ib.

Обратите также внимание, что аргументы, передаваемые в Fc, и аргументы, передаваемые в e, совпадают и являются временными. Преобразования могут выполняться поверх этого временного хранилища, чтобы привести его к форме, требуемой Fc, но, например, лямбда-функции нельзя привязать к другому типу делегата между Fc и e.

Открыть Вопрос

Это понижение означает, что последующие части интерполированной строки после вызова Append..., возвращающего значение false, не обрабатываются. Это может быть очень запутанным, особенно если форматное нарушение вызывает побочный эффект. Вместо этого мы могли бы сначала оценить все пробелы в формате, а затем повторно вызывать Append... с результатами, остановившись, если он возвращает false. Это гарантирует, что все выражения вычисляются в соответствии с ожиданиями, но мы вызываем ровно столько методов, сколько необходимо. Хотя частичное вычисление может быть желательно для некоторых более сложных случаев, это, возможно, не интуитивно понятно для общего случая.

Еще одна альтернатива, если мы хотим всегда оценивать все дыры формата, — удалить версию API Append... и просто выполнить повторяющиеся вызовы Format. Обработчик может отслеживать, следует ли удалить аргумент и немедленно вернуться к этой версии.

Ответ: Мы проведем условную оценку отверстий.

Открыть Вопрос

Нужно ли удалить типы удаленных обработчиков и упаковать вызовы с помощью try/finally, чтобы убедиться, что метод Dispose вызывается? Например, обработчик интерполированных строк в bcl может иметь арендованный массив внутри, и если один из интерполяционных элементов вызывает исключение во время выполнения, этот арендованный массив может утечь, если он не был освобожден.

Ответ: Нет. Обработчики могут быть назначены локальным переменным (например, MyHandler handler = $"{MyCode()};), а время существования таких обработчиков неясно. В отличие от foreach-перечислителей, где срок существования очевиден и не создается определяемая пользователем локальная переменная для перечислителя.

Влияние на типы ссылок, допускающие значение NULL

Чтобы свести к минимуму сложность реализации, у нас есть несколько ограничений на то, как мы выполняем анализ, допускающий значение NULL, для конструкторов интерполированных строковых обработчиков, используемых в качестве аргументов для метода или индексатора. В частности, мы не передаем информацию из конструктора обратно в оригинальные слоты параметров или аргументов из исходного контекста, и мы не используем типы параметров конструктора для определения универсального типа параметров в контейнерном методе. Пример того, где это может повлиять, —

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. Тем не менее, это не обойдёт необходимости в изменениях для улучшения преобразования выражения, хоть он и будет работать, это может привести к ненужным затратам.

ответ:

Мы считаем, что это может стать запутанным, и существует простое решение для пользовательских типов обработчиков: добавьте определяемое пользователем преобразование из строки.

Включение диапазонов для строк без использования кучи памяти

ValueStringBuilder в его текущем существовании имеет 2 конструктора: один, который принимает количество и выделяет оперативно в куче, и тот, который принимает Span<char>. Обычно размер Span<char> в кодовой базе среды выполнения фиксирован и составляет в среднем около 250 элементов. Чтобы действительно заменить этот тип, нам следует рассмотреть расширение, в рамках которого мы будем распознавать методы GetInterpolatedString, которые принимают Span<char>, а не только версию подсчета. Тем не менее, мы видим несколько потенциальных сложных случаев, которые нужно разрешить здесь.

  • Мы не хотим выполнять операцию stackalloc в производительном цикле. Если бы мы сделали это расширение функции, скорее всего, мы хотели бы разделить диапазон, выделенный с помощью stackalloc, между итерациями цикла. Мы знаем, что это безопасно, так как Span<T> является структурой ссылок, которая не может храниться в куче, и пользователям придется быть довольно изобретательными, чтобы удачно извлечь ссылку на эту Span (например, если создать метод, который принимает такой обработчик, а затем намеренно извлекает Span из обработчика и возвращает его вызывающему коду). Однако предварительное выделение ресурсов вызывает другие вопросы.
    • Должны ли мы использовать stackalloc с энтузиазмом? Что делать, если цикл никогда не вводится или завершает работу, прежде чем он нуждается в пространстве?
    • Если мы не стремимся стекалолок, это означает, что мы представляем скрытую ветвь на каждом цикле? Большинство циклов, вероятно, не будет обращать на это внимание, но это может повлиять на некоторые плотные циклы, которые не хотят платить стоимость.
  • Некоторые строки могут быть довольно большими, и соответствующая сумма stackalloc зависит от ряда факторов, включая факторы среды выполнения. Мы действительно не хотим, чтобы компилятор C# и спецификация должны были определить это заранее, поэтому мы хотели бы разрешить https://github.com/dotnet/runtime/issues/25423 и добавить API для компилятора для вызова в этих случаях. Он также добавляет больше плюсов и минусов к аргументам из предыдущего цикла, где мы не хотим потенциально многократно выделять большие массивы в куче или до того, как в них возникает необходимость.

Ответ:

Это не является областью для C# 10. Мы можем рассмотреть это в общем плане при изучении более общей функции params Span<T>.

Непробуемая версия API

Для простоты эта спецификация на данный момент просто предлагает распознавание метода Append..., и вещи, которые всегда успешны (например, InterpolatedStringHandler), всегда возвращают значение true из метода. Это было сделано для поддержки сценариев частичного форматирования, когда пользователь хочет прекратить форматирование, если возникает ошибка или если это ненужно, например, в случае ведения журнала, но это может потенциально привести к появлению множества ненужных ветвей в стандартном использовании интерполированных строк. Мы могли бы рассмотреть дополнение, в котором используем только методы FormatX, если метод Append... не присутствует, но возникают вопросы о том, что мы делаем, если присутствует сочетание вызовов 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, помещают значение , которое нужно отформатировать, перед элементом, в который необходимо это отформатировать. Чтобы лучше соответствовать этим шаблонам, мы, скорее всего, захотим это разрешить, но нам нужно решить, допустимо ли выполнение оценки в неправильном порядке.

Ответ:

Мы хотим поддержать это. Спецификация обновлена, чтобы отразить это. Аргументы должны быть указаны в лексическом порядке на сайте вызова, и если необходимый аргумент для метода создания указан после интерполированного строкового литерала, возникает ошибка.

await использование в интерполяционных отверстиях

Поскольку сегодня $"{await A()}" является допустимым выражением, необходимо рационализировать интерполяцию отверстий с использованием await. Это можно решить с помощью нескольких правил:

  1. Если интерполированная строка, используемая как string, IFormattableили FormattableString, содержит await в месте интерполяции, вернитесь к старому формату форматирования.
  2. Если интерполированная строка подвергается implicit_string_handler_conversion и applicable_interpolated_string_handler_type является ref struct, await не допускается использовать в отверстиях формата.

По сути, этот десугаринг может использовать ref структуру в асинхронном методе, если мы гарантируем, что ref struct не потребуется сохранять в куче, что будет возможно, если мы запретим awaitв интерполяционных отверстиях.

В качестве альтернативы можно просто сделать все типы обработчиков нессылочными структурами, включая обработчик интерполированной строки фреймворка. Однако это исключает возможность когда-нибудь признать версию Span, которая не нуждается в выделении свободного пространства вовсе.

ответ:

Мы будем обращаться с интерполированными обработчиками строк так же, как и с любым другим типом: это означает, что если тип обработчика является 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_expression компонентов слева направо.
  • cast_expression или relational_expression с оператором as, операндом которого является interpolated_string_expression, считается interpolated_string_expression для целей преобразования и разрешения перегрузки.

открытые вопросы:

Мы хотим сделать это? Мы не делаем это для System.FormattableString, например, но это может быть разбито на другую строку, в то время как это может быть зависимым от контекста и поэтому не может быть разбито на другую строку. Также нет проблем с разрешением перегрузки в отношении FormattableString и IFormattable.

ответ:

Мы считаем, что это обоснованный случай использования аддитивных выражений, но версия приведения в настоящее время не является достаточно убедительной. При необходимости его можно добавить позже. Спецификация обновлена, чтобы отразить это решение.

Другие варианты использования

Примеры предлагаемых API-интерфейсов обработчика с помощью этого шаблона см. в https://github.com/dotnet/runtime/issues/50635.