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).
- 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:
- 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:
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