Tutorial: Escrever um manipulador de interpolação de cadeia de caracteres personalizado
Neste tutorial, você aprenderá a:
- Implementar o padrão de manipulador de interpolação de cadeia de caracteres
- Interaja com o receptor em uma operação de interpolação de cadeia de caracteres.
- Adicionar argumentos ao manipulador de interpolação de cadeia de caracteres
- Entender os novos recursos de biblioteca para interpolação de cadeia de caracteres
Pré-requisitos
Você precisa configurar seu computador para executar o .NET. O compilador C# está disponível com o Visual Studio 2022 ou o SDK do .NET.
Este tutorial pressupõe que você esteja familiarizado com o C# e .NET, incluindo o Visual Studio ou a CLI do .NET.
Você pode escrever um manipulador personalizado de cadeia de caracteres interpolada . Um manipulador de cadeia de caracteres interpolado é um tipo que processa a expressão de espaço reservado em uma cadeia de caracteres interpolada. Sem um manipulador personalizado, os marcadores de posição são processados de forma semelhante a String.Format. Cada espaço reservado é formatado como texto e, em seguida, os componentes são concatenados para formar a cadeia de caracteres resultante.
Você pode escrever um manipulador para qualquer cenário em que use informações sobre a cadeia de caracteres resultante. Será usado? Quais restrições estão no formato? Alguns exemplos incluem:
- Você pode exigir que nenhuma das cadeias de caracteres resultantes seja maior que algum limite, como 80 caracteres. Você pode processar as cadeias de caracteres interpoladas para preencher um buffer de comprimento fixo e interromper o processamento depois que o comprimento do buffer for atingido.
- Você pode ter um formato tabular e cada espaço reservado deve ter um comprimento fixo. Um manipulador personalizado pode impor isso, em vez de forçar que todo o código do cliente esteja em conformidade.
Neste tutorial, você cria um manipulador de interpolação de cadeia de caracteres para um dos principais cenários de desempenho: registrar bibliotecas em log. Dependendo do nível de log configurado, o trabalho para construir uma mensagem de log não é necessário. Se o registro em log estiver desativado, o trabalho para construir uma cadeia de caracteres a partir de uma expressão de cadeia de caracteres interpolada não será necessário. A mensagem nunca é impressa, portanto, qualquer concatenação de cadeia de caracteres pode ser ignorada. Além disso, todas as expressões usadas nos espaços reservados, incluindo a geração de rastreamentos de pilha, não precisam ser feitas.
Um manipulador de cadeia de caracteres interpolado pode determinar se a cadeia de caracteres formatada será usada e só executará o trabalho necessário, se necessário.
Implementação inicial
Vamos começar de uma classe básica Logger
que dá suporte a diferentes níveis:
public enum LogLevel
{
Off,
Critical,
Error,
Warning,
Information,
Trace
}
public class Logger
{
public LogLevel EnabledLevel { get; init; } = LogLevel.Error;
public void LogMessage(LogLevel level, string msg)
{
if (EnabledLevel < level) return;
Console.WriteLine(msg);
}
}
Esse Logger
dá suporte a seis níveis diferentes. Quando uma mensagem não passa pelo filtro do nível de log, não há saída de dados. A API pública para o logger aceita uma string (totalmente formatada) como mensagem. Todo o trabalho para criar a string já está concluído.
Implementar o padrão de manipulador
Essa etapa é para criar um manipulador de cadeia de caracteres interpolado que recrie o comportamento atual. Um manipulador de cadeia de caracteres interpolado é um tipo que deve ter as seguintes características:
- O System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute aplicado ao tipo.
- Um construtor que tem dois parâmetros
int
,literalLength
eformattedCount
. (Mais parâmetros são permitidos). - Um método público
AppendLiteral
com a assinatura:public void AppendLiteral(string s)
. - Um método público
AppendFormatted
genérico com a assinatura:public void AppendFormatted<T>(T t)
.
Internamente, o construtor cria a cadeia de caracteres formatada e fornece um membro para um cliente recuperar essa cadeia de caracteres. O código a seguir mostra um tipo LogInterpolatedStringHandler
que atende a esses requisitos:
[InterpolatedStringHandler]
public struct LogInterpolatedStringHandler
{
// Storage for the built-up string
StringBuilder builder;
public LogInterpolatedStringHandler(int literalLength, int formattedCount)
{
builder = new StringBuilder(literalLength);
Console.WriteLine($"\tliteral length: {literalLength}, formattedCount: {formattedCount}");
}
public void AppendLiteral(string s)
{
Console.WriteLine($"\tAppendLiteral called: {{{s}}}");
builder.Append(s);
Console.WriteLine($"\tAppended the literal string");
}
public void AppendFormatted<T>(T t)
{
Console.WriteLine($"\tAppendFormatted called: {{{t}}} is of type {typeof(T)}");
builder.Append(t?.ToString());
Console.WriteLine($"\tAppended the formatted object");
}
internal string GetFormattedText() => builder.ToString();
}
Agora você pode adicionar uma sobrecarga a LogMessage
na classe Logger
para experimentar o novo manipulador de cadeia de caracteres interpolado:
public void LogMessage(LogLevel level, LogInterpolatedStringHandler builder)
{
if (EnabledLevel < level) return;
Console.WriteLine(builder.GetFormattedText());
}
Você não precisa remover o método original LogMessage
, o compilador prefere um método com um parâmetro de manipulador interpolado em vez de um método com um parâmetro string
quando o argumento for uma expressão de cadeia de caracteres interpolada.
Você pode verificar se o novo manipulador é invocado usando o seguinte código como o programa principal:
var logger = new Logger() { EnabledLevel = LogLevel.Warning };
var time = DateTime.Now;
logger.LogMessage(LogLevel.Error, $"Error Level. CurrentTime: {time}. This is an error. It will be printed.");
logger.LogMessage(LogLevel.Trace, $"Trace Level. CurrentTime: {time}. This won't be printed.");
logger.LogMessage(LogLevel.Warning, "Warning Level. This warning is a string, not an interpolated string expression.");
A execução do aplicativo produz uma saída semelhante ao seguinte texto:
literal length: 65, formattedCount: 1
AppendLiteral called: {Error Level. CurrentTime: }
Appended the literal string
AppendFormatted called: {10/20/2021 12:19:10 PM} is of type System.DateTime
Appended the formatted object
AppendLiteral called: {. This is an error. It will be printed.}
Appended the literal string
Error Level. CurrentTime: 10/20/2021 12:19:10 PM. This is an error. It will be printed.
literal length: 50, formattedCount: 1
AppendLiteral called: {Trace Level. CurrentTime: }
Appended the literal string
AppendFormatted called: {10/20/2021 12:19:10 PM} is of type System.DateTime
Appended the formatted object
AppendLiteral called: {. This won't be printed.}
Appended the literal string
Warning Level. This warning is a string, not an interpolated string expression.
Rastreando a saída, você pode ver como o compilador adiciona código para chamar o manipulador e criar a cadeia de caracteres:
- O compilador adiciona uma chamada para construir o manipulador, passando o comprimento total do texto literal na cadeia de caracteres de formato e o número de espaços reservados.
- O compilador adiciona chamadas a
AppendLiteral
eAppendFormatted
para cada seção da cadeia de caracteres literal e para cada espaço reservado. - O compilador invoca o método
LogMessage
usando oCoreInterpolatedStringHandler
como argumento.
Por fim, observe que o último aviso não invoca o manipulador de cadeia de caracteres interpolado. O argumento é um string
, de modo que a chamada invoca a outra sobrecarga com um parâmetro de cadeia de caracteres.
Importante
Use ref struct
para manipuladores de cadeias de caracteres interpoladas somente se absolutamente necessário. O uso de ref struct
terá limitações, pois elas devem ser armazenadas na pilha. Por exemplo, eles não funcionarão se uma lacuna de string interpolada contiver uma expressão await
, pois o compilador precisará guardar o manipulador na implementação IAsyncStateMachine
que foi gerada pelo compilador.
Adicionar mais recursos ao manipulador
A versão anterior do manipulador de cadeia de caracteres interpolada implementa o padrão. Para evitar o processamento de cada expressão de marcador de posição, você precisa de mais informações no gerenciador. Nesta seção, você melhora seu manipulador para que ele funcione menos quando a cadeia de caracteres construída não é gravada no log. Você usa System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute para especificar um mapeamento entre parâmetros para uma API pública e parâmetros para o construtor de um manipulador. Isso fornece ao manipulador as informações necessárias para determinar se a cadeia de caracteres interpolada deve ser avaliada.
Vamos começar com alterações no Manipulador. Primeiro, adicione um campo para acompanhar se o manipulador está habilitado. Adicione dois parâmetros ao construtor: um para especificar o nível de log dessa mensagem e o outro como referência ao objeto de log:
private readonly bool enabled;
public LogInterpolatedStringHandler(int literalLength, int formattedCount, Logger logger, LogLevel logLevel)
{
enabled = logger.EnabledLevel >= logLevel;
builder = new StringBuilder(literalLength);
Console.WriteLine($"\tliteral length: {literalLength}, formattedCount: {formattedCount}");
}
Em seguida, use o campo para que o manipulador acrescente somente literais ou objetos formatados quando a cadeia de caracteres final for usada:
public void AppendLiteral(string s)
{
Console.WriteLine($"\tAppendLiteral called: {{{s}}}");
if (!enabled) return;
builder.Append(s);
Console.WriteLine($"\tAppended the literal string");
}
public void AppendFormatted<T>(T t)
{
Console.WriteLine($"\tAppendFormatted called: {{{t}}} is of type {typeof(T)}");
if (!enabled) return;
builder.Append(t?.ToString());
Console.WriteLine($"\tAppended the formatted object");
}
Em seguida, você precisa atualizar a declaração LogMessage
para que o compilador passe os parâmetros adicionais para o construtor do manipulador. Isso é tratado usando System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute no argumento do manipulador:
public void LogMessage(LogLevel level, [InterpolatedStringHandlerArgument("", "level")] LogInterpolatedStringHandler builder)
{
if (EnabledLevel < level) return;
Console.WriteLine(builder.GetFormattedText());
}
Esse atributo especifica a lista de argumentos para LogMessage
que são mapeados para os parâmetros que seguem os parâmetros obrigatórios literalLength
e formattedCount
. A cadeia de caracteres vazia ("") especifica o receptor. O compilador substitui o valor do objeto Logger
representado pelo this
pelo argumento seguinte para o construtor do manipulador. O compilador substitui o valor de level
pelo argumento a seguir. Você pode fornecer qualquer número de argumentos para qualquer manipulador que escrever. Os argumentos que você adiciona são argumentos de cadeia de caracteres.
Você pode executar essa versão usando o mesmo código de teste. Desta vez, você vê os seguintes resultados:
literal length: 65, formattedCount: 1
AppendLiteral called: {Error Level. CurrentTime: }
Appended the literal string
AppendFormatted called: {10/20/2021 12:19:10 PM} is of type System.DateTime
Appended the formatted object
AppendLiteral called: {. This is an error. It will be printed.}
Appended the literal string
Error Level. CurrentTime: 10/20/2021 12:19:10 PM. This is an error. It will be printed.
literal length: 50, formattedCount: 1
AppendLiteral called: {Trace Level. CurrentTime: }
AppendFormatted called: {10/20/2021 12:19:10 PM} is of type System.DateTime
AppendLiteral called: {. This won't be printed.}
Warning Level. This warning is a string, not an interpolated string expression.
Você pode ver que os métodos AppendLiteral
e AppendFormat
estão sendo chamados, mas eles não estão realizando nenhuma tarefa. O manipulador determinou que a cadeia de caracteres final não é necessária e, portanto, o manipulador não a compila. Ainda há alguns aprimoramentos a serem feitos.
Primeiro, você pode adicionar uma sobrecarga do AppendFormatted
para restringir o argumento a um tipo que implementa System.IFormattable. Essa sobrecarga permite que os chamadores adicionem cadeias de caracteres de formato nos espaços reservados. Ao fazer essa alteração, também vamos alterar o tipo de retorno dos outros métodos AppendFormatted
e AppendLiteral
, de void
para bool
(se algum desses métodos tiver tipos de retorno diferentes, você receberá um erro de compilação). Essa alteração permite curto-circuito. Os métodos retornam false
para indicar que o processamento da expressão de cadeia de caracteres interpolada deve ser interrompido. Retornar true
indica que ele deve continuar. Neste exemplo, você está usando-o para interromper o processamento quando a cadeia de caracteres resultante não for necessária. O curto-circuito dá suporte a ações mais refinadas. Você pode parar de processar a expressão quando ela atingir um determinado comprimento, para dar suporte a buffers de comprimento fixo. Ou alguma condição pode indicar que elementos restantes não são necessários.
public void AppendFormatted<T>(T t, string format) where T : IFormattable
{
Console.WriteLine($"\tAppendFormatted (IFormattable version) called: {t} with format {{{format}}} is of type {typeof(T)},");
builder.Append(t?.ToString(format, null));
Console.WriteLine($"\tAppended the formatted object");
}
Com essa adição, você pode especificar cadeias de caracteres de formato em sua expressão de cadeia de caracteres interpolada:
var time = DateTime.Now;
logger.LogMessage(LogLevel.Error, $"Error Level. CurrentTime: {time}. The time doesn't use formatting.");
logger.LogMessage(LogLevel.Error, $"Error Level. CurrentTime: {time:t}. This is an error. It will be printed.");
logger.LogMessage(LogLevel.Trace, $"Trace Level. CurrentTime: {time:t}. This won't be printed.");
O :t
na primeira mensagem especifica o "formato de hora curto" para a hora atual. O exemplo anterior mostrou uma das sobrecargas para o método AppendFormatted
que você pode criar para o manipulador. Você não precisa especificar um argumento genérico para o objeto que está sendo formatado. Você pode ter maneiras mais eficientes de converter tipos que você cria em string. Você pode escrever sobrecargas de AppendFormatted
que adota esses tipos em vez de um argumento genérico. O compilador separa a melhor sobrecarga. O runtime usa essa técnica para converter System.Span<T> em saída de cadeia de caracteres. Você pode adicionar um parâmetro inteiro para especificar o alinhamento da saída, com ou sem um IFormattable. O System.Runtime.CompilerServices.DefaultInterpolatedStringHandler que é fornecido com o .NET 6 contém nove sobrecargas de AppendFormatted para usos diferentes. Você pode usá-lo como referência ao criar um manipulador para suas finalidades.
Execute o exemplo agora e você verá que, para a mensagem Trace
, apenas o primeiro AppendLiteral
é chamado:
literal length: 60, formattedCount: 1
AppendLiteral called: Error Level. CurrentTime:
Appended the literal string
AppendFormatted called: 10/20/2021 12:18:29 PM is of type System.DateTime
Appended the formatted object
AppendLiteral called: . The time doesn't use formatting.
Appended the literal string
Error Level. CurrentTime: 10/20/2021 12:18:29 PM. The time doesn't use formatting.
literal length: 65, formattedCount: 1
AppendLiteral called: Error Level. CurrentTime:
Appended the literal string
AppendFormatted (IFormattable version) called: 10/20/2021 12:18:29 PM with format {t} is of type System.DateTime,
Appended the formatted object
AppendLiteral called: . This is an error. It will be printed.
Appended the literal string
Error Level. CurrentTime: 12:18 PM. This is an error. It will be printed.
literal length: 50, formattedCount: 1
AppendLiteral called: Trace Level. CurrentTime:
Warning Level. This warning is a string, not an interpolated string expression.
Você pode fazer uma atualização final para o construtor do manipulador que melhora a eficiência. O manipulador pode adicionar um parâmetro final out bool
. Definir esse parâmetro para false
indica que o manipulador não deve ser chamado para processar a expressão de cadeia de caracteres interpolada:
public LogInterpolatedStringHandler(int literalLength, int formattedCount, Logger logger, LogLevel level, out bool isEnabled)
{
isEnabled = logger.EnabledLevel >= level;
Console.WriteLine($"\tliteral length: {literalLength}, formattedCount: {formattedCount}");
builder = isEnabled ? new StringBuilder(literalLength) : default!;
}
Essa alteração significa que você pode remover o campo enabled
. Em seguida, você pode alterar o tipo de retorno de AppendLiteral
e AppendFormatted
para void
.
Agora, ao executar o exemplo, você vê a seguinte saída:
literal length: 60, formattedCount: 1
AppendLiteral called: Error Level. CurrentTime:
Appended the literal string
AppendFormatted called: 10/20/2021 12:19:10 PM is of type System.DateTime
Appended the formatted object
AppendLiteral called: . The time doesn't use formatting.
Appended the literal string
Error Level. CurrentTime: 10/20/2021 12:19:10 PM. The time doesn't use formatting.
literal length: 65, formattedCount: 1
AppendLiteral called: Error Level. CurrentTime:
Appended the literal string
AppendFormatted (IFormattable version) called: 10/20/2021 12:19:10 PM with format {t} is of type System.DateTime,
Appended the formatted object
AppendLiteral called: . This is an error. It will be printed.
Appended the literal string
Error Level. CurrentTime: 12:19 PM. This is an error. It will be printed.
literal length: 50, formattedCount: 1
Warning Level. This warning is a string, not an interpolated string expression.
A única saída quando LogLevel.Trace
foi especificado é a saída do construtor. O manipulador indicou que não está habilitado, portanto, nenhum dos métodos Append
foi invocado.
Esse exemplo ilustra um ponto importante para manipuladores de cadeias de caracteres interpoladas, especialmente quando bibliotecas de log são usadas. Quaisquer efeitos colaterais nos espaços reservados podem não ocorrer. Adicione o seguinte código ao programa principal e veja esse comportamento em ação:
int index = 0;
int numberOfIncrements = 0;
for (var level = LogLevel.Critical; level <= LogLevel.Trace; level++)
{
Console.WriteLine(level);
logger.LogMessage(level, $"{level}: Increment index a few times {index++}, {index++}, {index++}, {index++}, {index++}");
numberOfIncrements += 5;
}
Console.WriteLine($"Value of index {index}, value of numberOfIncrements: {numberOfIncrements}");
Você pode ver que a variável index
é incrementada cinco vezes a cada iteração do loop. Como os espaços reservados são avaliados apenas para Critical
, Error
e Warning
níveis, não para Information
e Trace
, o valor final de index
não corresponde à expectativa:
Critical
Critical: Increment index a few times 0, 1, 2, 3, 4
Error
Error: Increment index a few times 5, 6, 7, 8, 9
Warning
Warning: Increment index a few times 10, 11, 12, 13, 14
Information
Trace
Value of index 15, value of numberOfIncrements: 25
Manipuladores de cadeia de caracteres interpolados fornecem maior controle sobre como uma expressão de cadeia de caracteres interpolada é convertida em uma cadeia de caracteres. A equipe de runtime do .NET usou esse recurso para melhorar o desempenho em várias áreas. Você pode usar a mesma funcionalidade em suas próprias bibliotecas. Para explorar mais, confira System.Runtime.CompilerServices.DefaultInterpolatedStringHandler. Ele fornece uma implementação mais completa do que a que você criou aqui. Você verá muitas outras sobrecargas possíveis para os métodos Append
.