Nos últimos posts, lancei o desafio para alterar a saída do programa a seguir para mostrar apenas linhas com números pares (leia as partes I e II se ainda não o fez).
using System.Runtime.CompilerServices; | |
void Foo(int someParam, string msg) | |
{ | |
if (!string.IsNullOrEmpty(msg)) | |
Console.WriteLine($"{someParam} - {msg}"); | |
} | |
for(int i = 0; i < 10; i++) | |
Foo(i, $"Test {i}"); |
- O using System.Runtime.CompilerServices que não é necessário, pois não estamos fazendo referência a nenhum tipo declarado nesse namespace.
- o if na linha 5 do método Foo() claramente não é necessário, pois o programa passa apenas strings não nulas/vazias para esse método.
Como sugeri, a solução que criei explora C# string interpolation handlers, um recurso introduzido no C# 10.
Munido com essas informações, uma maneira de atingir nosso objetivo consiste em simplesmente substituir o tipo de parâmetro msg por um custom string interpolation handler (InterpolatedHandlerTrick no código abaixo) da seguinte forma:
// https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/interpolated-string-handler | |
using System.Runtime.CompilerServices; | |
void Foo(int someParam, [InterpolatedStringHandlerArgument("someParam")] ref InterpolatedHandlerTrick msg) | |
{ | |
if (!string.IsNullOrEmpty(msg)) | |
Console.WriteLine($"{someParam} - {msg}"); | |
} | |
for(int i = 0; i < 10; i++) | |
Foo(i, $"Test {i}"); | |
[InterpolatedStringHandler] | |
ref struct InterpolatedHandlerTrick | |
{ | |
private DefaultInterpolatedStringHandler _handler; | |
public InterpolatedHandlerTrick(int literalLength, int formattedCount, int someParam, out bool willPrint) | |
{ | |
_handler = new DefaultInterpolatedStringHandler(literalLength, formattedCount); | |
willPrint = someParam % 2 == 0; | |
} | |
public static implicit operator string(in InterpolatedHandlerTrick o) => o.ToString(); | |
public void AppendLiteral(string msg) => _handler.AppendLiteral(msg); | |
public void AppendFormatted<T>(T value) => _handler.AppendFormatted(value); | |
public override string ToString() => _handler.ToString(); | |
public string ToStringAndClear() => _handler.ToStringAndClear(); | |
} |
- Declare um tipo e marque o mesmo com InterpolatedStringHandlerAttribute (linha #14)
- Implemente um construtor com pelo menos dois inteiros (literalLength e formattedCount) (nossa implementação declara alguns parâmetros extras para aproveitar alguns recursos mais avançados, que serão discutidos posteriormente)
- Implemente os métodos:
- void AppendLiteral(string msg)
- void AppendFormatted<T>(T value)
Se você colar este código no sharplab.io e selecionar C# no menu suspenso Results, você verá que o compiladorC# reescreveu seu programa (ou para manter a terminologia técnica desta transformação, o compilador Lowered o cópdigo), mais especificamente a chamada ao método Foo() para algo como:
for(int i = 0; i < 10; i++) | |
{ | |
bool willPrint; | |
InterpolatedHandlerTrick msg = new InterpolatedHandlerTrick(5, 1, i, out willPrint); | |
if (willPrint) | |
{ | |
msg.AppendLiteral("Test "); | |
msg.AppendFormatted(i); | |
} | |
Foo(i, ref msg); | |
} |
O código real será um pouco diferente (basicamente ele converterá o loop for em um while e introduzirá algumas variáveis locais extras), mas decidi ignorar isso e mantê-lo o mais próximo possível do original para fins de comparação.
Observe que o compilador introduziu uma variável local do tipo do nosso handler (linha 4) e fez algumas chamadas a métodos passando as várias partes da string interpolada; basicamente ele separou a string nos limites `{...}` e para cada parte, executou o método handler.AppendLiteral(part); (linha 7) seguido de handler.AppendFormatted(conteúdo da expressão {...}) (liha 8) .
Observe também que essas chamadas são agrupadas em um if (linha 5) controlado por uma variável que foi inicializada pelo construtor de nosso manipulador de string interpolado e voilà, temos todas as partes necessárias: o método que usa nosso manipulador especifica que um ou mais de seus parâmetros (neste caso apenas um) deve ser passado para o construtor do manipulador de string (através do InterpolatedStringHandlerArgumentAttribute) que ele usa para decidir se aquela string deve ser processada ou não, definindo um parâmetro ref bool (declarado como o último parâmetro do construtor) levando o código reduzido a pular as chamadas para os métodos AppendX() e, portanto, a instância do manipulador de string passado para Foo() produz uma string vazia que falha a condição (if linha 5 do programa inicial) ignorando a chamado ao método Console.WriteLine()!
Legal, mas antes de finalizar, aqui estão algumas melhorias/considerações de desempenho/usabilidade que esse recurso traz:
- DefaultInterpolatedStringHandler é, como o próprio nome indica, usado sempre que uma string interpolada é passada para um método que usa strings como parâmetro, portanto, simplesmente recompilar seu código baseado em C# 9 com C# 10 deve trazer pelo menos algumas melhorias de desempenho/alocações.
- Uma vez que DefaultInterpolatedStringHandler é declarado como uma ref struct, sua instanciação não causará uma alocação no heap (em geral, todos os Interpolated String Handlers devem ser uma ref struct para evitar tais alocações).
- Uma vez que String.Format() não esa mais sendo usado, nenhum array é alocado.
- As alocações podem ser completamente evitadas quando o argumento especificado por meio de InterpolatedStringHandlerArgumentAttribute define que a string resultante não será utilisada (muito útil em código do tipo logging/assertions)
- Esteja ciente que no caso do handler reportar que a string resultante não será utilizada (veja ponto anterior) side effects podem não ser observados; por exemplo, se nós modfificarmos o conteúdo da string interpolada (linha 10) de $"Test {i}" para $"Test {SomeMethod(i)}", o método SomeMethod() não será executado para valores ímpares de i o que não é óbvio apenas olhando a chamada ao método Foo() no código fonte to programa.
- Para minimizar ainda mais possíveis alocações no heap, considere implementar ISpanFormattable em seus próprios tipos, se eles puderem ser usados como expressões em strings interpoladas (varios tipos da BLC o fazem).
Divirta-se!
Adriano