Strings interpoladas melhoradas
Observação
Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele inclui mudanças de especificação propostas, juntamente com as informações necessárias durante o design e desenvolvimento do recurso. Estes artigos são publicados até que as alterações de especificações 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 Language Design Meeting (LDM).
Você pode saber mais sobre o processo de adoção de especificações de recursos no padrão de linguagem C# no artigo sobre as especificações .
Questão campeã: https://github.com/dotnet/csharplang/issues/4487
Resumo
Introduzimos um novo padrão para criar e usar expressões de cadeia de caracteres interpoladas para permitir a formatação e o uso eficientes em cenários de string
geral e cenários mais especializados, como estruturas de log, sem incorrer em alocações desnecessárias da formatação da cadeia de caracteres na estrutura.
Motivação
Hoje, a interpolação de strings reduz-se principalmente a uma chamada para string.Format
. Isto, embora de propósito geral, pode ser ineficiente por uma série de razões:
- Encapsula quaisquer argumentos struct, a menos que o ambiente de execução tenha introduzido uma sobrecarga de
string.Format
que acepte exatamente os tipos exatos de argumentos na ordem correta.- Esta ordenação é por isso que o tempo de execução hesita em introduzir versões genéricas do método, pois levaria à explosão combinatória de instanciações genéricas de um método muito comum.
- Ele tem que alocar uma matriz para os argumentos na maioria dos casos.
- Não há oportunidade de evitar a instanciação da instância se ela não for necessária. As estruturas de log, por exemplo, recomendam evitar a interpolação de strings porque isso fará com que uma string seja criada que pode não ser necessária, dependendo do nível de log atual da aplicação.
- Nunca se pode usar
Span
ou outros tipos de estruturas de referência hoje, porque estruturas de referência não são permitidas como tipos genéricos, o que significa que, se um utilizador quiser evitar copiar para locais intermediários, terá que formatar manualmente as strings.
Internamente, o tempo de execução tem um tipo chamado ValueStringBuilder
para ajudar a lidar com os 2 primeiros desses cenários. Eles passam um buffer alocado na pilha para o construtor, chamam repetidamente AppendFormat
com cada parte e, em seguida, obtêm uma string final. Se a cadeia de caracteres resultante ultrapassar os limites do buffer de pilha, eles podem movê-la para uma matriz na heap. No entanto, esse tipo é perigoso para expor diretamente, pois o uso incorreto pode levar uma matriz alugada a ser descartada duas vezes, o que resultará em comportamentos indefinidos no programa, já que dois locais acreditarem ter acesso exclusivo à matriz alugada. Esta proposta cria uma maneira de usar esse tipo com segurança a partir do código C# nativo apenas escrevendo uma string interpolada literal, deixando o código escrito inalterado enquanto melhora cada string interpolada que um usuário escreve. Este padrão também se estende para permitir que cadeias de caracteres interpoladas, passadas como argumentos para outros métodos, usem um padrão de manipulador definido pelo destinatário do método, o que permitirá que frameworks de registo evitem a alocação de cadeias de caracteres que nunca serão necessárias, proporcionando aos usuários de C# uma sintaxe de interpolação familiar e conveniente.
Projeto 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, examinamos o tipo do parâmetro. Se o tipo de parâmetro tiver um construtor que possa ser invocado com 2 parâmetros int, literalLength
e formattedCount
, opcionalmente aceite parâmetros adicionais especificados por um atributo no parâmetro original, e opcionalmente tenha um parâmetro booleano opcional na posição final, e o tipo do parâmetro original tiver métodos de instância AppendLiteral
e AppendFormatted
que possam ser invocados para cada parte da cadeia de caracteres interpolada, então processamos a interpolação usando isso, em vez de numa 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 string interpolada tem uma conversão implícita do manipulador para esse parâmetro, e ela diminui para o padrão mostrado acima. A especificação necessária para isso é um pouco complicada, e é expandida abaixo.
O resto da presente proposta utilizará Append...
para fazer referência a qualquer um dos AppendLiteral
ou AppendFormatted
nos casos em que ambos sejam 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()
{
}
}
}
Este 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; }
}
}
Este atributo é usado em parâmetros, para informar o compilador como diminuir um padrão de manipulador de cadeia de caracteres interpolado usado em uma posição de parâmetro.
Conversão interpolada do manipulador de cadeia de caracteres
Diz-se que o tipo T
é um applicable_interpolated_string_handler_type se for atribuído a System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute
.
Existe uma conversão implícita de 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 interpolada e utilizando apenas operadores +
.
Para simplicidade no resto deste speclet, interpolated_string_expression refere-se tanto a um interpolated_string_expressionsimples, como a uma additive_expression composta inteiramente de _interpolated_string_expressions_ e usando apenas +
operadores.
Observe que essa conversão sempre existe, independentemente de haver erros posteriores ao realmente tentar diminuir a interpolação usando o padrão do manipulador. Isso é feito para ajudar a garantir que haja erros previsíveis e úteis e que o comportamento do tempo de execução não seja alterado com base no conteúdo de uma cadeia de caracteres interpolada.
Ajustes aplicáveis ao membro da função
Ajustamos a redação do algoritmo do membro de função aplicável (§12.6.4.2) da seguinte forma: um novo sub-item é adicionado a cada seção, em negrito.
Diz-se que um membro de função é um membro de função aplicável em relação a uma lista de argumentos A
quando todos os itens a seguir forem verdadeiros:
- Cada argumento em
A
corresponde a um parâmetro na declaração do membro da função, conforme descrito em Parâmetros correspondentes (§12.6.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,ref
ouout
) é 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 struct, existe uma interpolated_string_handler_conversion implícita do argumento para o tipo do parâmetro correspondente, ou - Para um parâmetro
ref
ouout
, o tipo do argumento é idêntico ao tipo do parâmetro correspondente. Afinal, um parâmetroref
ouout
é um alias para o argumento passado.
Para um membro da função que inclui uma matriz de parâmetros, se o membro da função é aplicável pelas regras acima, diz-se que ele é aplicável em sua forma normal. Se um membro da função que inclui uma matriz de parâmetros não for aplicável em sua forma normal, o membro da função pode, em vez disso, ser aplicável em sua forma expandida:
- A forma expandida é construída substituindo a matriz de parâmetros na declaração de membro da função por zero ou mais parâmetros de valor do tipo de elemento da matriz de parâmetros, de modo que o número de argumentos na lista de argumentos
A
corresponda ao número total de parâmetros. SeA
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 pode ser construída e, portanto, não é aplicável. - Caso contrário, a forma expandida é 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 struct, existe uma interpolated_string_handler_conversion implícita do argumento para o tipo do parâmetro correspondente, ou - Para um parâmetro
ref
ouout
, o tipo do argumento é idêntico ao tipo do parâmetro correspondente.
Nota importante: isto significa que se existirem 2 sobrecargas equivalentes, que apenas diferem pelo tipo de applicable_interpolated_string_handler_type, estas sobrecargas serão consideradas ambíguas. Além disso, como não vemos através de moldes explícitos, é possível que possa surgir um cenário insolúvel onde ambas as sobrecargas aplicáveis usam InterpolatedStringHandlerArguments
e são totalmente inchamáveis sem executar manualmente o padrão de redução do manipulador. Poderíamos potencialmente fazer alterações no algoritmo de membro da função melhor para resolver isso, se assim o desejarmos, mas esse cenário provavelmente não ocorrerá e não é uma prioridade a ser abordada.
Melhor conversão a partir de ajustes de expressão
Alteramos a melhor conversão da expressão na seção (§12.6.4.5) para o seguinte:
Dada uma conversão implícita C1
que converte de uma expressão E
para um tipo T1
, e uma conversão implícita C2
que converte de uma expressão E
para um tipo T2
, C1
é um de conversão melhor do que C2
se:
-
E
é um interpolated_string_expressionnão constante,C1
é um implicit_string_handler_conversion,T1
é um applicable_interpolated_string_handler_type, eC2
não é um implicit_string_handler_conversion, ou -
E
não é exatamente igual aT2
e pelo menos uma das seguintes condições se aplica:
Isso significa que existem 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 simplesmente ser emitidas como constantes o façam e não incorram em nenhuma sobrecarga, enquanto as coisas que não podem ser constantes usem o padrão do manipulador.
InterpolatedStringHandler e Utilização
Introduzimos um novo tipo em System.Runtime.CompilerServices
: DefaultInterpolatedStringHandler
. Este é um ref struct com muitas das mesmas semânticas que ValueStringBuilder
, destinado ao uso direto pelo compilador C#. Esta 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);
}
}
Fazemos uma ligeira alteração às regras para o significado de um interpolated_string_expression (§12.8.3):
Se o tipo de uma cadeia interpolada for string
e o tipo System.Runtime.CompilerServices.DefaultInterpolatedStringHandler
existir, e o contexto atual suportar o uso desse tipo, a cadeiaé reduzida usando o padrão do tratador. O valor final string
é então obtido chamando ToStringAndClear()
no tipo de tratador.Caso contrário, se o tipo de cadeia interpolada for System.IFormattable
ou System.FormattableString
[o resto não será alterado]
A regra "e o contexto atual suporta o uso desse tipo" é intencionalmente vaga para dar ao compilador margem de manobra para otimizar o uso desse padrão. É provável que o tipo de manipulador seja um tipo ref struct, e os tipos ref struct normalmente não são permitidos em métodos assíncronos. Para este caso em particular, o compilador teria permissão para fazer uso do manipulador se nenhum dos orifícios 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 porque o manipulador será descartado depois que a expressão de cadeia de caracteres interpolada for avaliada.
Abrir Pergunta:
Queremos, em vez disso, apenas fazer com que o compilador saiba sobre DefaultInterpolatedStringHandler
e pular totalmente a chamada string.Format
? Isso nos permitiria esconder um método que não necessariamente queremos colocar na cara das pessoas quando elas chamam manualmente string.Format
.
Resposta: Sim.
Abrir Pergunta:
Queremos também configurar manipuladores para System.IFormattable
e System.FormattableString
?
Resposta: Não.
Codegen padrão do manipulador
Nesta seção, a resolução de invocação do método refere-se às etapas listadas no §12.8.10.2.
Resolução do construtor
Dado um applicable_interpolated_string_handler_typeT
e um interpolated_string_expressioni
, a resolução e validação de invocação de método para um construtor válido em T
é executada da seguinte maneira:
- A pesquisa de membros, por exemplo, construtores, é realizada em
T
. O grupo de métodos resultante é chamadoM
. - A lista de argumentos
A
é construída da seguinte forma:- Os dois primeiros argumentos são constantes inteiras, representando o comprimento literal de
i
, e o número de interpolação componentes emi
, respectivamente. - Se
i
for usado como um argumento para algum parâmetropi
no métodoM1
, e o parâmetropi
for atribuído comSystem.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute
, então para cada nomeArgx
na matrizArguments
desse atributo o compilador faz a correspondência com um parâmetropx
que tem o mesmo nome. A string vazia é correspondida ao recetor deM1
.- Se algum
Argx
não puder ser correspondido a um parâmetro deM1
, ou se umArgx
solicitar que o recetor deM1
eM1
seja um método estático, um erro é produzido e nenhuma outra etapa é tomada. - Caso contrário, o tipo de cada
px
resolvido será adicionado à lista de argumentos, na ordem especificada pela matrizArguments
. Cadapx
é passada com a mesma semânticaref
especificada emM1
.
- Se algum
- O argumento final é um
bool
, passado como um parâmetroout
.
- Os dois primeiros argumentos são constantes inteiras, representando o comprimento literal de
- A resolução tradicional de invocação de método é realizada com o grupo de métodos
M
e lista de argumentosA
. Para efeitos de validação final da invocação de método, o contexto deM
é tratado como uma member_access através do tipoT
.- Se um único melhor construtor
F
foi encontrado, o resultado da resolução de sobrecarga éF
. - Se nenhum construtor aplicável for encontrado, a etapa 3 será repetida, removendo o parâmetro
bool
final doA
. Se esta nova tentativa também não encontrar membros aplicáveis, um erro será produzido e nenhuma outra etapa será tomada. - Se nenhum método único foi encontrado, o resultado da resolução de sobrecarga é ambíguo, um erro é produzido e nenhuma etapa adicional é realizada.
- Se um único melhor construtor
- A validação final no
F
é realizada.- Se algum elemento de
A
ocorreu lexicamente apósi
, um erro é produzido e nenhuma outra etapa é tomada. - Se algum
A
solicitar o recetor deF
, eF
é um indexador sendo usado como um initializer_target em um member_initializer, então um erro é relatado e nenhuma outra etapa é tomada.
- Se algum elemento de
Nota: a resolução aqui intencionalmente não usar as expressões reais passadas como outros argumentos para Argx
elementos. 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 é vinculado a um tipo de delegado quando passado para M1
e vinculado a um tipo de delegado diferente quando passado para M
.
Nota: Relatamos um erro para indexadores usados como inicializadores de membros devido à ordem de avaliação dos inicializadores de membros aninhados. Considere este trecho 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 a __c1.C2[]
são avaliados antes de o recetor do indexador. Embora pudéssemos chegar a uma redução que funcione para esse cenário (criando um temp para __c1.C2
e compartilhando-o em ambas as invocações do indexador, ou apenas usando-o para a primeira invocação do indexador e compartilhando o argumento em ambas as invocações), achamos que qualquer redução seria confusa para o que acreditamos ser um cenário patológico. Portanto, proibimos totalmente o cenário.
Pergunta aberta:
Se utilizarmos um construtor em vez de Create
, melhoraremos a geração de código em tempo de execução, em troca de estreitar um pouco o padrão.
Resposta: Vamos restringir-nos aos construtores por enquanto. Podemos revisitar adicionar um método de Create
geral caso o cenário surja mais tarde.
Resolução de sobrecarga do método Append...
Dado um applicable_interpolated_string_handler_typeT
e um interpolated_string_expressioni
, a resolução de sobrecarga para um conjunto de métodos de Append...
válidos em T
é executada da seguinte forma:
- Se houver algum componente interpolated_regular_string_character no
i
:- A pesquisa de membros em
T
com o nomeAppendLiteral
é executada. O grupo de métodos resultante é chamadoMl
. - A lista de argumentos
Al
é construída com um parâmetro de valor do tipostring
. - A resolução da invocação de método tradicional é executada com o grupo de métodos
Ml
e a lista de argumentosAl
. Para fins de validação final de invocação de método, o contexto deMl
é tratado como uma member_access através de uma instância deT
.- Se um único melhor método
Fi
for encontrado e nenhum erro for gerado, o resultado da resolução de invocação do método seráFi
. - Caso contrário, um erro é reportado.
- Se um único melhor método
- A pesquisa de membros em
- Para cada componente de interpolação
ix
dei
:- A pesquisa de membros em
T
com o nomeAppendFormatted
é executada. O grupo de métodos resultante é chamadoMf
. - A lista de argumentos
Af
é construída:- O primeiro parâmetro é o
expression
deix
, passado por valor. - Se
ix
contiver diretamente um componente constant_expression, um parâmetro de valor inteiro será adicionado, com o nomealignment
especificado. - Se
ix
for seguido diretamente por um interpolation_format, um parâmetro de valor de cadeia de caracteres será adicionado, com o nomeformat
especificado.
- O primeiro parâmetro é o
- A resolução de invocação de método tradicional é executada com o grupo de método
Mf
e a lista de argumentosAf
. Para fins de validação final de invocação de método, o contexto deMf
é tratado como uma member_access através de uma instância deT
.- Se o melhor método
Fi
for encontrado, o resultado da resolução de invocação do método seráFi
. - Caso contrário, um erro é reportado.
- Se o melhor método
- A pesquisa de membros em
- Finalmente, para cada
Fi
descoberto nas etapas 1 e 2, a validação final é realizada:- Se algum
Fi
não devolverbool
por valor ouvoid
, será reportado um erro. - Se todos os
Fi
não retornarem o mesmo tipo, um erro será relatado.
- Se algum
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 do enumerador, onde 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 suportadas pelo idioma).
Temos regras de pesquisa de sobrecarga separadas para elementos base comparados aos orifícios de interpolação porque alguns manipuladores desejam poder entender a diferença entre os componentes que foram interpolados e os componentes que faziam parte da sequência de caracteres base.
Abrir Pergunta
Alguns cenários, como o registro estruturado, querem ser capazes de fornecer nomes para elementos de interpolação. Por exemplo, hoje uma chamada de registro pode se parecer com Log("{name} bought {itemCount} items", name, items.Count);
. Os nomes dentro do {}
fornecem informações de estrutura importantes para os registradores que ajudam a garantir que a saída seja consistente e uniforme. Alguns casos podem ser capazes de reutilizar o componente :format
de um orifício de interpolação para isso, mas muitos registadores já compreendem especificadores de formato e possuem comportamento pré-existente para a formatação de saída com base nessas informações. Existe alguma sintaxe que podemos usar para permitir colocar esses especificadores nomeados?
Alguns casos podem conseguir safar-se com CallerArgumentExpression
, desde que o suporte esteja disponível em C# 10. Mas para casos que invocam um método/propriedade, isso pode não ser suficiente.
Resposta:
Embora existam algumas partes interessantes para cadeias de caracteres modeladas que poderíamos explorar em um recurso de linguagem ortogonal, não achamos que uma sintaxe específica aqui tenha muito benefício sobre soluções como o uso de uma tupla: $"{("StructuredCategory", myExpression)}"
.
Realizar a conversão
Dado um applicable_interpolated_string_handler_typeT
e um interpolated_string_expressioni
que tinham Fc
construtores válidos e Append...
métodos Fa
resolvidos, a redução para i
é executada da seguinte forma:
- Quaisquer argumentos a
Fc
que ocorram lexicamente antes dei
são avaliados e armazenados em variáveis temporárias na ordem em que aparecem. A fim de preservar a ordenação lexical, sei
ocorreu como parte de uma expressão maiore
, quaisquer componentes dee
que ocorreram antes dei
serão avaliados também, novamente em ordem lexical. -
Fc
é chamado com o comprimento dos componentes literais da cadeia interpolada, o número de locais de interpolação , quaisquer argumentos avaliados anteriormente e um argumento de saídabool
(seFc
foi resolvido com um como o último parâmetro). O resultado é armazenado em um valor temporárioib
.- O comprimento dos componentes literais é calculado após a substituição de qualquer open_brace_escape_sequence com um único
{
e qualquer close_brace_escape_sequence com um único}
.
- O comprimento dos componentes literais é calculado após a substituição de qualquer open_brace_escape_sequence com um único
- Se o
Fc
terminar com o argumentobool
out, será gerada uma verificação desse valorbool
. Se for verdadeiro, os métodos emFa
serão chamados. Caso contrário, não serão chamados. - Para cada
Fax
emFa
,Fax
é chamado emib
com o componente literal atual ou pela expressão de interpolação , conforme seja apropriado. SeFax
retornar umbool
, o resultado será logicamente eredado com todas as chamadas deFax
anteriores.- Se
Fax
for uma chamada paraAppendLiteral
, o componente literal é desfeito substituindo qualquer sequência de escape de abre-chavetas por um único{
e qualquer sequência de escape de fecha-chavetas por um único}
.
- Se
- O resultado da conversão é
ib
.
Novamente, observe que os argumentos passados para Fc
e os argumentos passados para e
são os mesmos temp. As conversões podem ocorrer sobre o temp para converter para um formato que Fc
exige, mas, por exemplo, lambdas não podem ser associados a um tipo de delegado diferente entre Fc
e e
.
Abrir Pergunta
Essa redução significa que as partes subsequentes da cadeia de caracteres interpolada após uma chamada de Append...
de retorno falso não são avaliadas. Isso pode ser muito confuso, especialmente se a lacuna de formato tiver efeitos secundários. Em vez disso, poderíamos avaliar todas as lacunas de formato primeiro e depois chamar repetidamente Append...
com os resultados, parando se ele retornar false. Isso garantiria que todas as expressões fossem avaliadas como alguém poderia esperar, mas chamemos apenas os 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 furos de formato, é remover a versão Append...
da API e apenas fazer chamadas Format
repetidas. O manipulador pode controlar se ele deve simplesmente descartar o argumento e retornar imediatamente para esta versão.
Resposta: Faremos uma avaliação condicional dos buracos.
Questão Aberta Pergunta
Precisamos descartar tipos de manipuladores descartáveis e envolver chamadas com try/finally para garantir que Dispose seja chamado? Por exemplo, o manipulador de cadeia de caracteres interpolado na bcl pode ter uma matriz alugada dentro dela, e se um dos orifícios de interpolação lançar uma exceção durante a avaliação, essa matriz alugada poderá ser vazada se não for descartada.
Resposta: Não. os manipuladores podem ser atribuídos a locais (como MyHandler handler = $"{MyCode()};
), e a durabilidade desses manipuladores não é clara. Ao contrário dos enumeradores foreach, onde 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 transmitimos informações do construtor de volta para as posições originais dos parâmetros ou argumentos do contexto original, e não usamos os tipos de parâmetros do construtor para orientar a inferência de tipos genérica para parâmetros de tipo no método em que estão contidos. 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 de string
sejam conversíveis para manipuladores também.
Para facilitar a tarefa do autor, poderíamos considerar permitir que expressões do tipo string
sejam conversíveis de forma implícita para applicable_interpolated_string_handler_types. Conforme proposto hoje, os autores provavelmente precisarão sobrecarregar tanto esse tipo de manipulador quanto os tipos de string
regulares, para que seus usuários não precisem entender a diferença. Isso pode ser uma sobrecarga irritante e não óbvia, pois uma expressão string
pode ser vista como uma interpolação com expression.Length
comprimento pré-preenchido e 0 buracos a serem preenchidos.
Isso permitiria que novas APIs expusessem apenas um manipulador, sem ter também que expor uma sobrecarga que aceite string
. No entanto, ele não vai 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 fácil para tipos de manipuladores personalizados: adicionar uma conversão definida pelo usuário a partir da cadeia de caracteres.
Incorporando segmentos para strings sem uso de heap
ValueStringBuilder
como existe hoje tem 2 construtores: um que faz uma contagem, e aloca na pilha ansiosamente, e outro que leva um Span<char>
. O Span<char>
é geralmente um tamanho fixo na base de código em tempo de execução, com cerca de 250 elementos em média. Para verdadeiramente substituir esse tipo, devemos considerar uma extensão em que também reconheçamos métodos GetInterpolatedString
que aceitam um Span<char>
, em vez de apenas a versão de contagem. No entanto, vemos alguns casos potencialmente espinhosos para resolver aqui:
- Não queremos usar stackalloc repetidamente num hot loop. Se fôssemos fazer essa extensão para o recurso, provavelmente gostaríamos de compartilhar o stackalloc'd span entre iterações de loop. Sabemos que isso é seguro, pois
Span<T>
é uma estrutura ref que não pode ser armazenada na pilha, e os usuários teriam que ser bastante desonestos para conseguir extrair uma referência a esseSpan
(como criar um método que aceite tal manipulador e, em seguida, recuperar deliberadamente oSpan
do manipulador e devolvê-lo ao chamador). No entanto, a atribuição antecipada produz outras questões:- Devemos usar stackalloc com entusiasmo? E se o loop nunca for executado ou terminar antes de precisar do espaço?
- Se não utilizarmos stackalloc de forma eficiente, isso significa que introduzimos um ramo oculto em cada ciclo? A maioria dos loops provavelmente não irá se importar com isso, mas pode afetar alguns loops intensos que não querem arcar com o custo.
- Algumas cadeias de caracteres podem ser bastante grandes, e a quantidade apropriada para
stackalloc
depende de vários fatores, incluindo fatores de tempo de execução. Nós realmente não queremos que o compilador e a especificação do C# tenham que determinar isso com antecedência, então gostaríamos de resolver https://github.com/dotnet/runtime/issues/25423 e adicionar uma API para o compilador chamar nesses casos. Ele também adiciona mais prós e contras aos pontos do loop anterior, onde não queremos alocar matrizes grandes na pilha muitas vezes ou antes que uma seja necessária.
Resposta:
Isso está fora do escopo do C# 10. Podemos analisar isto de forma geral quando analisamos o recurso params Span<T>
mais geral.
Versão não experimentada da API
Para simplificar, esta especificação atualmente apenas propõe reconhecer um método Append...
, e coisas que sempre têm sucesso (como InterpolatedStringHandler
) sempre retornariam verdadeiras 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 o caso de registro, mas pode potencialmente introduzir um monte de ramificações desnecessárias no uso padrão de cadeia de caracteres interpolada. Poderíamos considerar um adendo em que usamos apenas métodos FormatX
se nenhum método Append...
estiver presente, mas levanta questões sobre o que fazemos se houver mistura de chamadas Append...
e FormatX
.
Resposta:
Queremos a versão não experimental da API. A proposta foi atualizada para refletir este facto.
Passando argumentos anteriores para o manipulador
Há uma lamentável falta de simetria na proposta em que ela existe atualmente: invocar um método de extensão de forma reduzida produz semânticas diferentes de invocar o método de extensão na forma normal. Isso é diferente da maioria dos outros locais na língua, onde a forma reduzida é apenas um açúcar. Propomos adicionar um atributo à estrutura que reconheceremos ao vincular um método, que informa ao compilador que certos 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 a que temos de responder:
- Gostamos deste padrão em geral?
- Queremos permitir que esses argumentos venham de depois do parâmetro handler? Alguns padrões existentes na BCL, como
Utf8Formatter
, posicionam o valor a ser formatado antes do elemento necessário para a formatação. Para alinhar melhor com esses padrões, provavelmente queremos permitir isto, mas precisamos decidir se essa avaliação fora de ordem é aceitável.
Resposta:
Queremos apoiá-lo. A especificação foi atualizada para refletir isso. Os argumentos deverão ser especificados em ordem lexical no local de chamada e, se um argumento necessário para o método create for especificado após o literal da cadeia de caracteres interpolada, um erro será produzido.
await
utilização em lacunas de interpolação
Porque $"{await A()}"
é uma expressão válida hoje, precisamos racionalizar os buracos de interpolação com espera. Poderíamos resolver este problema com algumas regras:
- Se uma cadeia de caracteres interpolada usada como
string
,IFormattable
ouFormattableString
tiver umawait
em um orifício de interpolação, volte para o formatador antigo. - Se uma string 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 nas lacunas de formato.
Fundamentalmente, este desdobramento poderia usar um ref struct num método assíncrono, desde que garantamos que o ref struct
não precisará ser salvo na heap, o que deve ser possível se proibirmos await
nos locais de interpolação.
Como alternativa, poderíamos simplesmente tornar todos os tipos de manipuladores em estruturas que não são de referência, incluindo o manipulador de framework para cadeias de caracteres interpoladas. No entanto, isso impedir-nos-ia de um dia reconhecer uma versão Span
que não precisa de alocar qualquer 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 um ref struct e o contexto atual não permitir o uso de ref structs, é ilegal usar handler aqui. A especificação em torno da redução de literais de cadeia usados como strings é intencionalmente vaga para permitir que o compilador decida sobre quais regras considera apropriadas, mas para tipos de manipuladores personalizados, terão que seguir as mesmas regras do restante da linguagem.
Manipuladores como parâmetros de referência
Alguns gestores de eventos podem necessitar ser passados como parâmetros ref (in
ou ref
). Devemos permitir uma ou outra? E, em caso afirmativo, como será um gestor de ref
?
ref $""
é confuso, como você não está realmente passando a cadeia de caracteres por ref, você está passando o manipulador que é criado a partir da ref por ref, e tem problemas potenciais semelhantes com métodos assíncronos.
Resposta:
Queremos apoiá-lo. 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.
Strings interpoladas através do uso de expressões binárias e conversões
Como esta proposta torna as cadeias interpoladas sensíveis ao contexto, gostaríamos de permitir que o compilador trate uma expressão binária composta inteiramente de cadeias interpoladas, ou uma cadeia interpolada submetida a um elenco, como uma cadeia interpolada literal para fins de resolução de sobrecarga. Por exemplo, considere 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, exigindo uma conversão necessária para Handler1
ou Handler2
para resolver. No entanto, ao realizar essa conversão, potencialmente perderíamos a informação de que existe um contexto do recetor do método, o que significa que a conversão falharia porque não há nada para preencher as informações de c
. Um problema semelhante surge com a concatenação binária de cadeias de caracteres: o utilizador poderia querer formatar o literal em várias linhas para evitar a quebra de linha, mas não seria capaz porque isso não seria mais uma cadeia de caracteres interpolada conversível para o tipo de gestor.
Para resolver esses casos, fazemos as seguintes alterações:
- Um additive_expression composto inteiramente por interpolated_string_expressions e que utiliza apenas operadores
+
é considerado um interpolated_string_literal para efeitos de conversões e resolução de sobrecargas. A cadeia interpolada final é criada pela concatenação lógica de todos os componentes individuais 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 interpolated_string_expressions para fins de conversões e resolução de sobrecarga.
Perguntas Abertas:
Queremos fazê-lo? Não fazemos isso para System.FormattableString
, por exemplo, mas isso pode ser separado para uma linha diferente, enquanto que este pode depender do contexto e, portanto, não pode ser separado para uma linha diferente. Também não há problemas de resolução de sobrecarga com FormattableString
e IFormattable
.
Resposta:
Achamos que este é um caso de uso válido para expressões aditivas, mas que a versão cast não é convincente o suficiente neste momento. Podemos adicioná-lo mais tarde, se necessário. A especificação foi atualizada para refletir esta 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.
C# feature specifications