Partilhar via


Tutorial: Escrever um manipulador de interpolação de cadeia de caracteres personalizado

Neste tutorial, você aprenderá a:

  • Implemente o padrão de manipulador de interpolação de strings.
  • Interaja com o recetor numa operação de interpolação de cadeias.
  • Adiciona argumentos ao manipulador de interpolação de cadeias.
  • Compreenda os novos recursos da biblioteca para interpolação de strings.

Pré-requisitos

Configura a tua máquina para correr .NET. O compilador C# está disponível através do Visual Studio ou do SDK .NET.

Este tutorial assume que está familiarizado com C# e .NET, incluindo Visual Studio ou Visual Studio Code e o C# DevKit.

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 handler personalizado, o sistema processa placeholders 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. Considere perguntas como: É usado? Quais são as restrições do formato? Eis alguns exemplos:

  • Você pode exigir que nenhuma das cadeias de caracteres resultantes seja maior do 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 assim que esse comprimento de buffer for atingido.
  • Você pode ter um formato tabular e cada espaço reservado deve ter um comprimento fixo. Um handler personalizado pode impor essa restrição, em vez de forçar todo o código cliente a conformar-se.

Neste tutorial, você cria um manipulador de interpolação de cadeia de caracteres para um dos principais cenários de desempenho: bibliotecas de log. Dependendo do nível de log configurado, o trabalho para construir uma mensagem de log não é necessário. Se o registo estiver desativado, não é necessário o trabalho para construir uma string a partir de uma expressão de string interpolada. A mensagem nunca é impressa, portanto, qualquer concatenação de cadeia de caracteres pode ser ignorada. Além disso, quaisquer expressões usadas nos marcadores de lugar, incluindo a geração de traços de pilha, não precisam de ser feitas.

Um handler de cadeias interpoladas pode determinar se a cadeia formatada é usada e só realizar o trabalho necessário se necessário.

Execução inicial

Comece com uma classe básica Logger que suporte 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);
    }
}

Este Logger suporta seis níveis diferentes. Quando uma mensagem não passa o filtro de nível logarítmico, o logger não produz saída. A API pública do logger aceita uma string totalmente formatada como mensagem. O chamador faz todo o trabalho para criar a string.

Implementar o padrão do manipulador

Neste passo, constróis um handler de string interpolado que recria 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 e formattedCount. (Mais parâmetros são permitidos).
  • Um método público de AppendLiteral com a assinatura: public void AppendLiteral(string s).
  • Um método público genérico AppendFormatted com a assinatura: public void AppendFormatted<T>(T t).

Internamente, o construtor cria a cadeia formatada e fornece um membro para o cliente recuperar essa cadeia. O código a seguir mostra um tipo de 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");
    }

    public override string ToString() => builder.ToString();
}

Observação

Quando a expressão de string interpolada é uma constante em tempo de compilação (ou seja, não possui marcadores de posição), o compilador utiliza o tipo de destino string em vez de invocar um manipulador de string interpolada personalizado. Este comportamento provoca que as strings interpoladas constantes ignorem totalmente os manipuladores personalizados.

Agora você pode adicionar uma sobrecarga a LogMessage na classe Logger para tentar seu novo manipulador de cadeia de caracteres interpolada:

public void LogMessage(LogLevel level, LogInterpolatedStringHandler builder)
{
    if (EnabledLevel < level) return;
    Console.WriteLine(builder.ToString());
}

Não precisas de remover o método original LogMessage . Quando o argumento é uma expressão de strings interpolada, o compilador prefere um método com um parâmetro handler interpolado em vez de um método com um string parâmetro.

Pode verificar que o novo handler é invocado usando o seguinte código como 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 através da saída, você pode ver como o compilador adiciona código para chamar o manipulador e construir a cadeia de caracteres:

  • O compilador adiciona uma chamada para construir o manipulador, passando o comprimento total do texto literal na string de formato e o número de marcadores de posição.
  • O compilador adiciona chamadas a AppendLiteral e AppendFormatted para cada seção da string literal e para cada placeholder.
  • O compilador invoca o método LogMessage usando o CoreInterpolatedStringHandler como argumento.

Finalmente, observe que o último aviso não invoca o manipulador de cadeia de caracteres interpolada. O argumento é um string, de modo que a chamada invoca a outra sobrecarga com um parâmetro string.

Importante

Use ref struct para manipuladores de cadeia de caracteres interpolados somente se absolutamente necessário. ref struct os tipos têm limitações, pois devem ser armazenados na stack. Por exemplo, não funcionam se um espaço de string interpolada contiver uma await expressão, porque o compilador precisa de armazenar o handler na implementação gerada pelo compilador IAsyncStateMachine.

Adicionar mais recursos ao manipulador

A versão anterior do manipulador de cadeia de caracteres interpolado implementa o padrão. Para evitar o processamento de todas as expressões de espaço reservado, você precisa de mais informações no manipulador. Nesta seção, você melhora seu manipulador para que ele faça menos trabalho quando a cadeia de caracteres construída não é gravada no log. Utilize System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute para especificar um mapeamento entre parâmetros de uma API pública e parâmetros do construtor de um gestor. Esse mapeamento fornece ao manipulador a informação necessária para determinar se a cadeia interpolada deve ser avaliada.

Começa com alterações ao handler. Primeiro, adicione um campo para controlar se o manipulador estiver habilitado. Adicione dois parâmetros ao construtor: um para especificar o nível de log para esta mensagem e o outro uma 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}");
}

De seguida, use o campo para que o seu handler só acrescente literais ou objetos formatados quando a cadeia 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");
}

De seguida, atualiza a LogMessage declaração para que o compilador passe os parâmetros adicionais ao construtor do handler. Trate deste passo usando o argumento System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute no manipulador:

public void LogMessage(LogLevel level, [InterpolatedStringHandlerArgument("", "level")] LogInterpolatedStringHandler builder)
{
    if (EnabledLevel < level) return;
    Console.WriteLine(builder.ToString());
}

Este atributo especifica a lista de argumentos para LogMessage que se correlacionam com os parâmetros que seguem os parâmetros literalLength e formattedCount necessários. A cadeia vazia (""), especifica o recetor. O compilador substitui o valor do objeto Logger representado por this para o próximo argumento para o construtor do manipulador. O compilador substitui o valor de level para o seguinte argumento. Você pode fornecer qualquer número de argumentos para qualquer manipulador que você criar. Os argumentos que você adiciona são argumentos de cadeia de caracteres.

Observação

Se a InterpolatedStringHandlerArgumentAttribute lista de argumentos do construtor estiver vazia, o comportamento é o mesmo que se o atributo fosse completamente omitido.

Pode correr esta versão usando o mesmo código de teste. Desta vez, você verá 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.

Vês que os métodos AppendLiteral e AppendFormat são chamados, mas não estão a fazer qualquer trabalho. O manipulador determinou que a cadeia de caracteres final não é necessária, portanto, o manipulador não a cria. Há ainda algumas melhorias a fazer.

Primeiro, você pode adicionar uma sobrecarga de AppendFormatted que restringe 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 esta alteração, também altere o tipo de retorno do outro AppendFormatted e AppendLiteral métodos, de void para bool. Se algum destes métodos tiver tipos de retorno diferentes, recebe um erro de compilação. Esta alteração permite o curto-circuito de . Os métodos retornam false para indicar que o processamento da expressão de cadeia de caracteres interpolada deve ser interrompido. O retorno true indica que deve continuar. Neste exemplo, você está usando-o para parar o processamento quando a cadeia de caracteres resultante não é necessária. A técnica de curto-circuito suporta ações mais refinadas. Você pode parar de processar a expressão quando ela atingir um determinado comprimento, para oferecer suporte a buffers de comprimento fixo. Ou alguma condição pode indicar que os 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");
}

public void AppendFormatted<T>(T t, int alignment, string format) where T : IFormattable
{
    Console.WriteLine($"\tAppendFormatted (IFormattable version) called: {t} with alignment {alignment} and format {{{format}}} is of type {typeof(T)},");
    var formatString =$"{alignment}:{format}";
    builder.Append(string.Format($"{{0,{formatString}}}", t));
    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 tempo curto" para a hora atual. O exemplo anterior mostrou uma das sobrecargas para o método AppendFormatted que você pode criar para seu manipulador. Não é necessário especificar um argumento genérico para o objeto que está sendo formatado. Você pode ter maneiras mais eficientes de converter tipos criados em cadeia de caracteres. Você pode escrever sobrecargas de AppendFormatted que usem esses tipos em vez de um argumento genérico. O compilador escolhe a melhor sobrecarga. O tempo de execução usa essa técnica para converter System.Span<T> em string. Você pode adicionar um parâmetro inteiro para especificar o alinhamento da saída, com ou sem um IFormattable. O System.Runtime.CompilerServices.DefaultInterpolatedStringHandler fornecido com o .NET 6 contém nove sobrecargas de AppendFormatted para diferentes usos. Você pode usá-lo como uma referência ao criar um manipulador para seus propósitos.

Execute o exemplo agora e você verá que, para a mensagem Trace, apenas a primeira AppendLiteral é chamada:

        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 no construtor do manipulador que melhora a eficiência. O manipulador pode adicionar um parâmetro out bool final. Definir esse parâmetro como 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, quando você executa 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 é especificado é a saída do construtor. O handler indicou que não está ativado, por isso nenhum dos Append métodos é invocado.

Este exemplo ilustra um ponto importante para manipuladores de cadeia de caracteres interpolados, especialmente quando bibliotecas de log são usadas. Quaisquer efeitos secundários nos marcadores de posição podem não ocorrer. Adicione o seguinte código ao seu 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 {index++}");
    numberOfIncrements++;
}
Console.WriteLine($"Value of index {index}, value of numberOfIncrements: {numberOfIncrements}");

Pode ver que a index variável é incrementada a cada iteração do ciclo. Como os marcadores 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 0
Error
Error: Increment index 1
Warning
Warning: Increment index 2
Information
Trace
Value of index 3, value of numberOfIncrements: 5

Os 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 tempo de execução do .NET usou esse recurso para melhorar o desempenho em várias áreas. Você pode usar o mesmo recurso em suas próprias bibliotecas. Para explorar mais, veja o System.Runtime.CompilerServices.DefaultInterpolatedStringHandler. Ele fornece uma implementação mais completa do que você construiu aqui. Você vê muito mais sobrecargas que são possíveis para os métodos Append.