Compartilhar via


Cadeias de caracteres interpoladas aprimoradas

Nota

Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele inclui alterações de especificação propostas, juntamente com as informações necessárias durante o design e o desenvolvimento do recurso. Esses artigos são publicados até que as alterações de especificação propostas sejam finalizadas e incorporadas na especificação ECMA atual.

Pode haver algumas discrepâncias entre a especificação do recurso e a implementação concluída. Essas diferenças são capturadas nas notas pertinentes da reunião de design de idioma (LDM).

Você pode saber mais sobre o processo de adoção de speclets de recursos no padrão de linguagem C# no artigo sobre as especificações de .

Problema do especialista: https://github.com/dotnet/csharplang/issues/4487

Resumo

Apresentamos um novo padrão para criar e usar expressões de string interpoladas para permitir uma formatação e uso eficientes tanto em cenários gerais de string quanto em cenários mais especializados, como frameworks de registro, sem incorrer em alocações desnecessárias resultantes da formatação da string no framework.

Motivação

Hoje, a interpolação de cadeias de caracteres se resume principalmente a uma chamada para string.Format. Isso, embora seja de uso geral, pode ser ineficiente por vários motivos:

  1. Ela agrupa quaisquer argumentos de struct, a menos que o runtime tenha introduzido uma sobrecarga de string.Format que aceita exatamente os tipos corretos de argumentos na ordem exata.
    • Essa ordenação é a razão pela qual o runtime está hesitante em introduzir versões genéricas do método, pois isso levaria à explosão combinatória de instanciações genéricas de um método muito comum.
  2. Na maioria dos casos, ela precisa alocar uma matriz para os argumentos.
  3. Não há oportunidade de evitar a instanciação da instância se ela não for necessária. As estruturas de registros, por exemplo, recomendam evitar a interpolação de cadeias de caracteres, pois ela fará com que seja criada uma cadeia de caracteres, que pode não ser necessária, dependendo do nível de registro atual da aplicação.
  4. Ele nunca pode usar Span ou outros tipos de struct ref hoje, pois os structs ref não são permitidos como parâmetros de tipo genérico, o que significa que, se um usuário quiser evitar copiar para locais intermediários, ele precisará formatar manualmente cadeias de caracteres.

Internamente, o runtime tem um tipo chamado ValueStringBuilder para ajudar a lidar com os dois primeiros desses cenários. Elas passam um buffer alocado na pilha para o construtor, chamam AppendFormat repetidamente com cada parte e, depois, geram uma cadeia de caracteres final. Se essa cadeia de caracteres resultante ultrapassar os limites do buffer alocado na pilha, poderá ser transferida para uma matriz no heap. No entanto, é perigoso expor esse tipo diretamente porque o uso incorreto pode fazer com que uma matriz alugada seja liberada duas vezes, o que pode causar todo tipo de comportamento indefinido no programa pelo fato de dois locais acreditarem ter acesso exclusivo à matriz alugada. Essa proposta cria uma maneira de usar esse tipo com segurança no código C# nativo apenas escrevendo uma string interpolada literal, mantendo o código escrito inalterado, melhorando cada string interpolada que um usuário escreve. Ele também estende esse padrão para permitir que cadeias de caracteres interpoladas, passadas como argumentos para outros métodos, usem um padrão de manipulador definido pelo receptor do método em questão. Isso permitirá que frameworks de registro de logs evitem alocar cadeias de caracteres que nunca serão necessárias e proporcionará aos usuários do C# uma sintaxe de interpolação familiar e conveniente.

Design detalhado

O padrão do manipulador

Apresentamos um novo padrão de manipulador que pode representar uma cadeia de caracteres interpolada passada como um argumento para um método. O inglês simples do padrão é o seguinte:

Quando um interpolated_string_expression é passado como um argumento para um método, analisamos o tipo do parâmetro. Se o tipo de parâmetro tem um construtor que pode ser invocado com dois parâmetros int, literalLength e formattedCount, e que opcionalmente usa parâmetros adicionais especificados por um atributo no parâmetro original e um parâmetro booleano de retorno, e o tipo do parâmetro original tem métodos de instância AppendLiteral e AppendFormatted que podem ser invocados para cada parte da cadeia de caracteres interpolada, então reduzimos a interpolação usando isso em vez de utilizar uma chamada tradicional para string.Format(formatStr, args). Um exemplo mais concreto é útil para imaginar isso:

// 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);

Aqui, como TraceLoggerParamsInterpolatedStringHandler tem um construtor com os parâmetros corretos, dizemos que a cadeia de caracteres interpolada tem uma conversão de manipulador implícita para esse parâmetro e reduz para o padrão mostrado acima. As especificações necessárias para isso são um pouco complicadas e são expandidas abaixo.

O restante desta proposta usará Append... para se referir a um dos AppendLiteral ou AppendFormatted nos casos em que ambos forem aplicáveis.

Novos atributos

O compilador reconhece o 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()
        {
        }
    }
}

Esse atributo é usado pelo compilador para determinar se um tipo é um tipo de manipulador de cadeia de caracteres interpolado válido.

O compilador também reconhece o 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; }
    }
}

Esse atributo é usado em parâmetros, para informar ao compilador como reduzir um padrão de manipulador de cadeia de caracteres interpolado usado em uma posição de parâmetro.

Conversão do manipulador de cadeia de caracteres interpolada

O tipo T é dito ser um applicable_interpolated_string_handler_type se for atribuído a System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute. Existe uma conversão implícita interpolated_string_handler para T a partir de uma expressão de string interpolada , ou uma expressão aditiva composta inteiramente por expressões de string interpoladas e usando apenas operadores +.

Para simplificar o restante deste speclet, interpolated_string_expression faz referência tanto a um interpolated_string_expression simples quanto a um additive_expression composto inteiramente de _interpolated_string_expression_s e usando apenas operadores +.

Observe que essa conversão sempre existe, independentemente de haver erros posteriores ao tentar reduzir a interpolação usando o padrão de manipulador. Isso é feito para ajudar a garantir que haja erros previsíveis e úteis e que o comportamento do runtime não seja alterado com base no conteúdo de uma cadeia de caracteres interpolada.

Ajustes aplicáveis ao membro funcional

Ajustamos a redação do algoritmo de membro da função aplicável (§12.6.4.2) da seguinte maneira (um novo sub-marcador foi adicionado a cada seção, em negrito):

Um membro da função é considerado um membro da função aplicável em relação a uma lista de argumentos A quando todos os seguintes são verdadeiros:

  • Cada argumento em A corresponde a um parâmetro na declaração do membro da função, conforme descrito nos parâmetros correspondentes (§12.6.2.2.2), e qualquer parâmetro ao qual nenhum argumento corresponde é um parâmetro opcional.
  • Para cada argumento em A, o modo de passagem de parâmetro do argumento (ou seja, valor, refou out) é idêntico ao modo de passagem de parâmetro do parâmetro correspondente e
    • para um parâmetro de valor ou uma matriz de parâmetros, existe uma conversão implícita (§10.2) do argumento para o tipo do parâmetro correspondente ou
    • para um parâmetro ref cujo tipo é um tipo de struct, existe um interpolated_string_handler_conversion implícito do argumento para o tipo do parâmetro correspondente ou
    • para um parâmetro ref ou out, o tipo do argumento é idêntico ao tipo do parâmetro correspondente. Afinal, um parâmetro ref ou out é um alias para o argumento passado.

Para um membro de função que inclui um array de parâmetros, se o membro de função for aplicável pelas regras acima, diz-se que ele é aplicável na sua forma normal . Se um membro de função que inclui uma matriz de parâmetros não for aplicável em sua forma normal, o membro da função poderá, em vez disso, ser aplicável em seu formulário expandido:

  • O formulário expandido é construído substituindo a matriz de parâmetros na declaração do membro da função por parâmetros de valor zero ou mais do tipo de elemento da matriz de parâmetros, de modo que o número de argumentos na lista de argumentos A corresponde ao número total de parâmetros. Se A tiver menos argumentos do que o número de parâmetros fixos na declaração do membro da função, a forma expandida do membro da função não poderá ser construída e, portanto, não será aplicável.
  • Caso contrário, o formulário expandido será aplicável se para cada argumento em A o modo de passagem de parâmetro do argumento for idêntico ao modo de passagem de parâmetro do parâmetro correspondente e
    • para um parâmetro de valor fixo ou um parâmetro de valor criado pela expansão, existe uma conversão implícita (§10.2) do tipo do argumento para o tipo do parâmetro correspondente ou
    • para um parâmetro ref cujo tipo é um tipo de struct, existe um interpolated_string_handler_conversion implícito do argumento para o tipo do parâmetro correspondente ou
    • para um parâmetro ref ou out, o tipo do argumento é idêntico ao tipo do parâmetro correspondente.

Observação importante: isso significa que, se houver duas sobrecargas equivalentes, que diferem apenas pelo tipo de applicable_interpolated_string_handler_type, essas sobrecargas serão consideradas ambíguas. Além disso, como não levamos em conta as conversões explícitas, talvez possa surgir um cenário sem solução em que as sobrecargas aplicáveis usam InterpolatedStringHandlerArguments e são totalmente incontestáveis sem executar manualmente o padrão de redução do manipulador. Poderíamos potencialmente fazer alterações no algoritmo de membro de função melhor para resolver isso se assim escolhermos, mas esse cenário dificilmente ocorrerá e não é uma prioridade a ser resolvida.

Melhor conversão de ajustes de expressão

Alteramos a melhor conversão da seção de expressão (§12.6.4.5) para o seguinte:

Considerando uma conversão implícita C1 que converte de uma expressão E em um tipo T1e uma conversão implícita C2 que converte de uma expressão E em um tipo T2, C1 é uma conversão melhor do que C2 se:

  1. E é um interpolated_string_expression não constante, C1 é um implicit_string_handler_conversion, T1 é um applicable_interpolated_string_handler_type e C2 não é um implicit_string_handler_conversion, ou
  2. E não corresponde exatamente a T2, e pelo menos um dos seguintes se aplica:
    • E corresponde exatamente T1 (§12.6.4.5)
    • T1 é um destino de conversão melhor do que T2 (§12.6.4.6)

Isso significa que há algumas regras de resolução de sobrecarga potencialmente não óbvias, dependendo se a cadeia de caracteres interpolada em questão é uma expressão constante ou não. Por exemplo:

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

Isso é introduzido para que as coisas que podem ser simplesmente emitidas como constantes façam isso e não incorram em nenhuma sobrecarga, enquanto coisas que não podem ser constantes usam o padrão de manipulador.

InterpolatedStringHandler e o uso

Apresentamos um novo tipo em System.Runtime.CompilerServices: DefaultInterpolatedStringHandler. Este é um struct ref com muitas das mesmas semânticas que ValueStringBuilder, destinadas ao uso direto pelo compilador C#. Essa estrutura seria aproximadamente assim:

// 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);
    }
}

Fizemos uma pequena alteração nas regras quanto ao significado de um interpolated_string_expression (§12.8.3):

Se o tipo de uma cadeia de caracteres interpolada for string e o tipo System.Runtime.CompilerServices.DefaultInterpolatedStringHandler existir, e o contexto atual oferecer suporte ao uso desse tipo, a cadeia de caracteresserá reduzida usando o padrão de manipulador. O valor final do string é obtido chamando ToStringAndClear() no tipo de manipulador.Caso contrário, se o tipo de uma cadeia de caracteres interpolada for System.IFormattable ou System.FormattableString [o restante não será alterado]

A regra "e o contexto atual dá suporte ao uso desse tipo" é intencionalmente vaga para dar margem de manobra ao compilador na otimização do uso desse padrão. É provável que o manipulador seja do tipo struct ref, e esses tipos normalmente não são permitidos em métodos assíncronos. Para esse caso específico, o compilador poderá usar o manipulador se nenhum dos buracos de interpolação contiver uma expressão await, pois podemos determinar estaticamente que o tipo de manipulador é usado com segurança sem análise complicada adicional, pois o manipulador será descartado após a avaliação da expressão de cadeia de caracteres interpolada.

Pergunta Aberta:

Queremos, em vez disso, fazer com que o compilador reconheça DefaultInterpolatedStringHandler e ignore totalmente a chamada string.Format? Isso nos permitiria ocultar um método que não queremos necessariamente colocar na cara das pessoas quando elas chamam manualmente string.Format.

Resposta: Sim.

Pergunta Aberta:

Também queremos ter handlers para System.IFormattable e System.FormattableString?

Resposta: Não.

Codegen de padrão do manipulador

Nesta seção, a resolução de invocação de método refere-se às etapas listadas em §12.8.10.2.

Resolução do construtor

Considerando um applicable_interpolated_string_handler_typeT e um interpolated_string_expressioni, a resolução de invocação de método e a validação de um construtor válido em T são realizadas da seguinte maneira:

  1. A pesquisa de membros de construtores de instância é executada em T. O grupo de métodos resultante é chamado M.
  2. A lista de argumentos A é construída da seguinte maneira:
    1. Os dois primeiros argumentos são constantes de inteiro, representando o comprimento literal de i e o número de componentes da interpolação em i, respectivamente.
    2. Se i for usado como argumento para algum parâmetro pi no método M1, e o parâmetro pi for atribuído com System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute, então, para cada nome Argx no array Arguments desse atributo, o compilador o corresponderá a um parâmetro px que tenha o mesmo nome. A cadeia de caracteres vazia é correspondida ao receptor de M1.
      • Se qualquer Argx não puder ser correspondida a um parâmetro de M1 ou um Argx solicitar o receptor de M1 e M1 for um método estático, ocorrerá um erro e nenhuma outra etapa será executada.
      • Caso contrário, o tipo de cada px resolvido é adicionado à lista de argumentos, na ordem especificada pela matriz Arguments. Cada px é passado com a mesma semântica ref especificada em M1.
    3. O argumento final é um bool, passado como um parâmetro out.
  3. A resolução de invocação de método tradicional é executada com o grupo de métodos M e a lista de argumentos A. Para fins de validação final da invocação de método, o contexto de M é tratado como member_access por meio do tipo T.
    • Se for encontrado um melhor construtor único F, o resultado da resolução de sobrecarga será F.
    • Se nenhum construtor aplicável for encontrado, a etapa 3 deverá ser repetida, removendo o parâmetro bool final do A. Se essa repetição também não encontrar membros aplicáveis, será gerado um erro e nenhuma outra etapa será executada.
    • Se nenhum método único melhor foi encontrado, o resultado da resolução de sobrecarga é ambíguo, um erro é produzido e nenhuma etapa adicional é tomada.
  4. A validação final em F é executada.
    • Se algum elemento de A ocorreu lexicamente após i, um erro é produzido e nenhuma etapa adicional é executada.
    • Se qualquer A solicitar o receptor de F e F for um indexador sendo usado como initializer_target em um member_initializer, será relatado um erro e nenhuma outra etapa será executada.

Observação: intencionalmente, a resolução aqui não usa as expressões reais passadas como outros argumentos para os elementos de Argx. Consideramos apenas os tipos pós-conversão. Isso garante que não tenhamos problemas de conversão dupla ou casos inesperados em que um lambda esteja associado a um tipo delegado quando passado para M1 e associado a um tipo delegado diferente quando passado para M.

Observação: relatamos um erro referente a indexadores usados como inicializadores de membro devido à ordem de avaliação de inicializadores de membro aninhados. Considere este snippet de código:


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 { }
    }
}

Os argumentos para __c1.C2[] são avaliados antes do receptor do indexador. Embora possamos criar uma redução que funcione para esse cenário (criando uma temporária para __c1.C2 e compartilhando-a nas duas invocações do indexador ou apenas usando-a para a primeira invocação do indexador e compartilhando o argumento entre ambas as invocações), concluímos que uma redução seria confusa para o que acreditamos ser um cenário patológico. Portanto, proibimos totalmente o cenário.

pergunta aberta:

Se usarmos um construtor em vez de Create, melhoraremos o codegen de runtime, às custas de restringir um pouco o padrão.

Resposta: Por enquanto, restringiremos aos construtores. Podemos reconsiderar a adição de um método Create geral posteriormente se essa necessidade surgir.

Resolução de sobrecarga do método Append...

Considerando um applicable_interpolated_string_handler_typeT e um interpolated_string_expressioni, a resolução de sobrecarga de um conjunto de métodos Append... válidos em T é executada da seguinte maneira:

  1. Se houver componentes de interpolated_regular_string_character em i:
    1. A pesquisa de membros em T com o nome AppendLiteral será realizada. O grupo de métodos resultante é chamado Ml.
    2. A lista de argumentos Al é construída com um parâmetro de valor do tipo string.
    3. A resolução de invocação de método tradicional é executada com o grupo de métodos Ml e a lista de argumentos Al. Para fins de validação final da invocação de método, o contexto de Ml é tratado como member_access por meio de uma instância de T.
      • Se um único melhor método Fi for encontrado e nenhum erro for produzido, o resultado da resolução de invocação de método será Fi.
      • Do contrário, um erro será relatado.
  2. Para cada componente da interpolaçãoix de i:
    1. A pesquisa de membros em T com o nome AppendFormatted será realizada. O grupo de métodos resultante é chamado Mf.
    2. A lista de argumentos Af é construída:
      1. O primeiro parâmetro é o expression de ix, passado por valor.
      2. Se ix contiver diretamente um componente constant_expression, um parâmetro de valor inteiro será adicionado, com o nome alignment especificado.
      3. Se ix for seguido diretamente por um interpolation_format, um parâmetro de valor de cadeia de caracteres será adicionado, com o nome format especificado.
    3. A resolução de invocação de método tradicional é executada com o grupo de métodos Mf e a lista de argumentos Af. Para fins de validação final da invocação de método, o contexto de Mf é tratado como member_access por meio de uma instância de T.
      • Se for encontrado um melhor método único Fi, o resultado da resolução da invocação do método será Fi.
      • Do contrário, um erro será relatado.
  3. Por fim, para cada Fi descoberto nas etapas 1 e 2, a validação final é executada:
    • Se algum Fi não retornar bool por valor ou void, um erro será relatado.
    • Se todos os Fi não retornarem o mesmo tipo, um erro será relatado.

Observe que essas regras não permitem métodos de extensão para as chamadas Append.... Poderíamos considerar habilitar isso se escolhermos, mas isso é análogo ao padrão de enumerador, em que permitimos que GetEnumerator seja um método de extensão, mas não Current ou MoveNext().

Essas regras permitem parâmetros padrão para as chamadas Append..., que funcionarão com coisas como CallerLineNumber ou CallerArgumentExpression (quando houver suporte para o idioma).

Temos regras de pesquisa de sobrecarga separadas para elementos base versus buracos de interpolação, pois alguns manipuladores desejarão entender a diferença entre os componentes que foram interpolados e os que faziam parte da string base.

Pergunta aberta

Alguns cenários, como o registro em log estruturado, querem poder fornecer nomes para elementos de interpolação. Por exemplo, hoje uma chamada de registro em log pode se parecer com Log("{name} bought {itemCount} items", name, items.Count);. Os nomes dentro de {} fornecem informações importantes sobre a estrutura para os agentes que ajudam a garantir a consistência e a uniformidade da saída. Alguns casos podem reutilizar o componente :format de um espaço de interpolação para isso, mas muitos agentes já entendem os especificadores de formato e têm um comportamento existente para a formatação da saída com base nessas informações. Há alguma sintaxe que podemos usar para habilitar a colocação desses especificadores nomeados?

Alguns casos podem ser resolvidos usando CallerArgumentExpression, desde que o suporte seja em C# 10. Mas para casos que invocam um método/propriedade, isso pode não ser suficiente.

Resposta:

Embora as cadeias de caracteres de modelo tenham algumas partes interessantes que poderíamos explorar em um recurso de linguagem ortogonal, não consideramos que uma sintaxe específica aqui tenha muito benefício em relação a soluções como o uso de uma tupla: $"{("StructuredCategory", myExpression)}".

Executando a conversão

Dado um applicable_interpolated_string_handler_typeT e um interpolated_string_expressioni que tinham um construtor Fc válido e métodos Append... Fa resolvidos, a redução de i é executada da seguinte maneira:

  1. Todos os argumentos para Fc que ocorrem lexicamente antes de i são avaliados e armazenados em variáveis temporárias em ordem lexical. Para preservar a ordenação lexical, se i ocorreu como parte de uma expressão maior e, todos os componentes de e que ocorreram antes de i também serão avaliados, novamente em ordem lexical.
  2. Fc é chamado com o comprimento dos componentes literais da cadeia de caracteres interpolada, o número de espaços de interpolação, quaisquer argumentos avaliados anteriormente e um argumento de saída bool (se Fc foi resolvido com um argumento como o último parâmetro). O resultado é armazenado em um valor temporário ib.
    1. O comprimento dos componentes literais é calculado depois de substituir qualquer open_brace_escape_sequence por um único {e qualquer close_brace_escape_sequence por um único }.
  3. Se Fc terminar com um argumento de saída bool, será gerada uma verificação desse valor bool. Se retornarem true, os métodos em Fa serão chamados. Caso contrário, eles não serão chamados.
  4. Para cada Fax em Fa, Fax é chamado em ib com o componente literal atual ou a expressão de interpolação, conforme apropriado. Se Fax retornar um bool, o resultado será combinado logicamente com todas as chamadas anteriores de Fax.
    1. Se Fax for uma chamada para AppendLiteral, o componente literal será desserializado substituindo cada open_brace_escape_sequence por um único { e cada close_brace_escape_sequence por um único }.
  5. O resultado da conversão é ib.

Novamente, observe que os argumentos passados para Fc e os argumentos passados para e são os mesmos temporários. As conversões podem ocorrer para além dos temporários, para converter para um formato exigido por Fc, mas, por exemplo, não é possível associar lambdas a um tipo de delegado diferente entre Fc e e.

Pergunta Aberta

Essa redução significa que as partes subsequentes da cadeia de caracteres interpolada após uma chamada Append... que retorna false não são avaliadas. Isso pode ser muito confuso, especialmente se a lacuna de formato estiver causando efeitos colaterais. Em vez disso, primeiro nós poderíamos avaliar todos os espaços de formato e, então, chamar Append... repetidamente com os resultados, parando se retornar false. Isso garantiria que todas as expressões fossem avaliadas como esperado, mas chamamos o mínimo de métodos necessários. Embora a avaliação parcial possa ser desejável para alguns casos mais avançados, talvez não seja intuitiva para o caso geral.

Outra alternativa, se quisermos sempre avaliar todos os orifícios de formato, é remover a versão Append... da API e apenas fazer chamadas Format repetidas. O manipulador pode controlar se deve apenas remover o argumento e retornar imediatamente para essa versão.

Resposta: teremos uma avaliação condicional dos buracos.

Pergunta aberta

Precisamos descartar tipos de manipuladores descartáveis e encapsular chamadas com try/finally para garantir que Dispose seja chamado? Por exemplo, o manipulador de cadeia de caracteres interpolada em bcl pode ter uma matriz alugada dentro dele e, se um dos espaços de interpolação gerar uma exceção durante a avaliação, essa matriz alugada poderá ser vazada se não tiver sido descartada.

Resposta: Não. os manipuladores podem ser atribuídos a locais (como MyHandler handler = $"{MyCode()};), e o tempo de vida desses manipuladores não está claro. Ao contrário dos enumeradores foreach, em que o tempo de vida é óbvio e nenhum local definido pelo usuário é criado para o enumerador.

Impacto nos tipos de referência anuláveis

Para minimizar a complexidade da implementação, temos algumas limitações sobre como executamos a análise anulável em construtores de manipulador de cadeia de caracteres interpolados usados como argumentos para um método ou indexador. Em particular, não transferimos informações do construtor de volta aos slots originais de parâmetros ou argumentos do contexto original e não usamos tipos de parâmetro de construtor para guiar a inferência de tipos genéricos para parâmetros de tipo no método que os contém. Um exemplo de onde isso pode ter um impacto é:

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()
    {
    }
}

Outras considerações

Permitir que tipos string sejam conversíveis em manipuladores também

Para simplificar a criação de tipos, poderíamos considerar permitir que expressões do tipo string sejam implicitamente conversíveis em applicable_interpolated_string_handler_types. Conforme proposto hoje, provavelmente os autores precisarão sobrecarregar esse tipo de manipulador e tipos string regulares para que os usuários não precisem entender a diferença. Isso pode ser uma sobrecarga incômoda e não óbvia, pois uma expressão string pode ser vista como interpolação com comprimento pré-preenchido expression.Length e nenhum espaço a ser preenchido.

Isso permitiria que novas APIs expusessem apenas um manipulador, sem também precisar expor uma sobrecarga que aceite string. No entanto, ele não contornará a necessidade de alterações para uma melhor conversão da expressão, portanto, embora funcione, pode ser uma sobrecarga desnecessária.

Resposta:

Achamos que isso pode acabar sendo confuso, e há uma solução alternativa fácil para tipos de manipulador personalizados: adicionar uma conversão definida pelo usuário a partir de uma string.

Incorporando intervalos para cadeias de caracteres sem heap

ValueStringBuilder, na forma como existe hoje, tem dois construtores, sendo um que usa uma contagem e aloca no heap prontamente e outro que usa um Span<char>. Geralmente, esse Span<char> tem um tamanho fixo na base de código de runtime, aproximadamente 250 elementos em média. Para substituir de fato esse tipo, devemos considerar uma extensão disso em que também reconhecemos os métodos GetInterpolatedString que usam Span<char>, em vez de apenas a versão da contagem. No entanto, vemos alguns possíveis casos espinhosos a serem resolvidos aqui:

  • Não queremos usar "stackalloc" repetidamente em um loop crítico. Se fizéssemos essa extensão para o recurso, provavelmente compartilharíamos o intervalo de alocação na pilha entre as iterações de loop. Sabemos que isso é seguro, pois Span<T> é um struct ref que não pode ser armazenado no heap, e os usuários teriam que ser bastante ardilosos para conseguir extrair uma referência para esse Span (como criar um método que aceita tal handler e, em seguida, deliberadamente recuperar o Span do handler e devolvê-lo ao chamador). No entanto, alocar antecipadamente produz outras perguntas:
    • Devemos fazer alocação na pilha prontamente? E se o loop nunca acontecer ou sair antes de precisar do espaço?
    • Se não fizermos a alocação na pilha prontamente, significa que introduzimos um branch oculto em cada loop? A maioria dos loops provavelmente não se importará com isso, mas alguns loops rígidos que não querem pagar o custo podem ser afetados.
  • Algumas cadeias de caracteres podem ser muito grandes e a quantidade apropriada para stackalloc depende de vários fatores, incluindo fatores de runtime. Não queremos que o compilador e a especificação do C# tenham que determinar isso com antecedência, portanto, gostaríamos de resolver https://github.com/dotnet/runtime/issues/25423 e adicionar uma API para o compilador chamar nesses casos. Isso também gera mais prós e contras em relação aos pontos do loop anterior, pois não queremos alocar grandes matrizes no heap muitas vezes ou antes de uma ser necessária.

Resposta:

Isso está fora do escopo do C# 10. Podemos examinar isso de maneira geral quando consideramos o recurso params Span<T> de forma mais abrangente.

Versão não experimental da API

Para simplificar, no momento essa especificação apenas propõe reconhecer um método Append... e elementos que sempre são dão certo (como InterpolatedStringHandler) sempre retornarão true do método. Isso foi feito para dar suporte a cenários de formatação parcial em que o usuário deseja interromper a formatação se ocorrer um erro ou se for desnecessário, como no caso de log, mas isso poderia potencialmente introduzir várias ramificações desnecessárias no uso padrão de cadeia de caracteres interpolada. Podemos considerar um adendo em que usamos apenas os métodos FormatX se nenhum método Append... estiver presente, mas ele pode apresentar perguntas sobre o que fazemos se houver uma mistura de chamadas Append... e FormatX.

Resposta:

Queremos a versão definitiva da API. A proposta foi atualizada para refletir isso.

Passando argumentos anteriores para o manipulador

Atualmente, há uma infeliz falta de simetria na proposta: invocar um método de extensão em forma reduzida produz semântica diferente do que invocar o método de extensão na forma normal. Isso é diferente da maioria dos outros locais em termos de linguagem, em que a forma reduzida é apenas uma simplificação. Propomos adicionar um atributo à estrutura que reconheceremos ao associar um método, que informa ao compilador que determinados parâmetros devem ser passados para o construtor no manipulador. O uso tem esta aparência:

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; }
    }
}

O uso disso é então:

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);

As perguntas que precisamos responder:

  1. Gostamos desse padrão em geral?
  2. Desejamos permitir que esses argumentos venham depois do parâmetro do manipulador? Alguns padrões existentes no BCL, como Utf8Formatter, colocam o valor a ser formatado antes do que precisa ser formatado. Para nos ajustarmos melhor a esses padrões, provavelmente queremos permitir isso, mas precisamos decidir se essa avaliação fora de ordem é aceitável.

Resposta:

Queremos dar suporte a isso. A especificação foi atualizada para refletir isso. Os argumentos precisarão ser especificados em ordem lexical no local da chamada e, se um argumento necessário para o método criar for especificado após o literal da cadeia de caracteres interpolada, um erro será produzido.

Uso de await em espaços de interpolação

Como $"{await A()}" é uma expressão válida hoje, precisamos racionalizar os espaços de interpolação com await. Poderíamos resolver isso com algumas regras:

  1. Se uma cadeia de caracteres interpolada usada como string, IFormattable ou FormattableString tiver um await em um espaço de interpolação, volte para o formatador no estilo antigo.
  2. Se uma cadeia de caracteres interpolada estiver sujeita a um implicit_string_handler_conversion e applicable_interpolated_string_handler_type for um ref struct, await não poderá ser usado nos espaços de formato.

Fundamentalmente, esse processo de simplificação pode utilizar um struct ref em um método assíncrono, desde que possamos garantir que ref struct não precise ser salvo no heap, o que deve ser possível se proibirmos awaitnos espaços de interpolação.

Como alternativa, poderíamos simplesmente fazer todos os tipos de manipuladores serem non-ref structs, incluindo o manipulador do framework para cadeias de caracteres interpoladas. No entanto, isso nos impediria de um dia reconhecer uma versão Span que não precisa alocar espaço temporário.

Resposta:

Trataremos os manipuladores de cadeia de caracteres interpolados da mesma forma que qualquer outro tipo: isso significa que, se o tipo de manipulador for uma struct de referência e o contexto atual não permitir o uso de structs de referência, será ilegal utilizar o manipulador neste caso. A especificação em torno da redução de literais de cadeia de caracteres usados como cadeias de caracteres é intencionalmente vaga para permitir que o compilador decida quais regras considera apropriadas, mas para tipos de manipuladores personalizados eles terão que seguir as mesmas regras que o restante da linguagem.

Manipuladores como parâmetros ref

Alguns manipuladores devem ser passados como parâmetros ref (seja in ou ref). Devemos permitir algum dos dois? E, nesse caso, como será um manipulador ref? ref $"" é confuso porque, na verdade, você não está passando a cadeia de caracteres por ref, mas sim o manipulador que é criado a partir de ref por ref e que tem problemas em potencial semelhantes com os métodos assíncronos.

Resposta:

Queremos dar suporte a isso. A especificação foi atualizada para refletir isso. As regras devem refletir as mesmas regras que se aplicam aos métodos de extensão em tipos de valor.

Interpolação de cadeias de caracteres por meio de expressões binárias e conversões

Como essa proposta torna o contexto de cadeias de caracteres interpoladas sensível, gostaríamos de permitir que o compilador trate uma expressão binária composta inteiramente de cadeias de caracteres interpoladas, ou uma cadeia de caracteres interpolada sujeita a uma conversão, como um literal de cadeia de caracteres interpolada para fins da resolução de sobrecarga. Por exemplo, veja o seguinte cenário:

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

Isso seria ambíguo, com a necessidade de uma conversão para Handler1 ou Handler2 para resolver. No entanto, ao fazer essa conversão, potencialmente descartaríamos as informações de que há um contexto do receptor do método, o que significa que a conversão falharia porque não haveria nada para preencher as informações de c. Ocorre um problema semelhante com a concatenação binária de cadeias de caracteres: o usuário poderia querer formatar o literal em várias linhas para evitar a quebra automática de linha, mas não conseguiria porque não seria mais um literal de cadeia de caracteres interpolado conversível para o tipo de manipulador.

Para resolver esses casos, fazemos as seguintes alterações:

  • Um additive_expression composto inteiramente por interpolated_string_expressions e usando somente operadores + é considerado um interpolated_string_literal para fins de conversões e da resolução de sobrecarga. A cadeia de caracteres interpolada final é criada concatenando logicamente todos os componentes individuais de interpolated_string_expression, da esquerda para a direita.
  • Um cast_expression ou um relational_expression com operador as cujo operando é um interpolated_string_expressions é considerado um interpolated_string_expressions para fins de conversões e da resolução de sobrecarga.

perguntas abertas:

Queremos fazer isso? Não fazemos isso para System.FormattableString, por exemplo, mas pode ser colocado em outra linha, enquanto isso pode depender do contexto e, portanto, não pode ser colocado em outra linha. Também não há preocupações de resolução de sobrecarga com FormattableString e IFormattable.

Resposta:

Consideramos que esse é um caso de uso válido para expressões aditivas, mas que no momento a versão de conversão não é convincente o suficiente. Podemos adicioná-lo mais tarde, se necessário. A especificação foi atualizada para refletir essa decisão.

Outros casos de uso

Consulte https://github.com/dotnet/runtime/issues/50635 para obter exemplos de APIs de manipulador propostas usando esse padrão.