Esse é o 2º post da série sobre C# 7. Pra acompanhar a série você pode seguir a tag C#7 no blog ou voltar no post que agrega a série.
Nesse post vou falar de local functions, ou funções locais, que nada mais são do que funções dentro de funções. Assim como na funcionalidade comentada no post anterior, Tuplas, essa funcionalidade é mais uma que ajuda a deixar o código bem mais limpo, além de trazer uns bons ganhos de desempenho.
A funcionalidade está bem estável no momento, já está no branch “future” do Roslyn, e é praticamente certo de que vai entrar, o time já está finalizando os trabalhos, mas ainda há diversos bugs.
Você pode ler o issue no github que sugere local functions no repositório do Roslyn, é o issue chamado “Proposal: Nested local functions and type declarations”, e a funcionalidade é acompanhada por uma tag chamada “New Language Feature – Local Functions” que agrega sugestões em volta da proposta principal. O documento de especificação da funcionalidade está também no branch “future”, e pode ser visto em local-functions.md. O trabalho pendente está em local-functions.work.md.
Como será uma função local? Sem mais suspense, aqui um exemplo retirado dos testes:
class Program { static void Main(string[] args) { void Local() { Console.WriteLine(""Hello, world!""); } Local(); } }
Da linha 5 a 8 você vê a função local declarada, é uma função simples, que não retorna nada, apenas escreve pra console. Ela é chamada logo após a declaração, na linha 9.
O que será possível fazer com local functions
Algumas observações. Será possível:
- criar funções assíncronas, que poderão esperar com await;
- usar iterators com yield em funções anonimas;
- aninhar quantas vezes quanto necessário, ou seja, uma função local pode conter outra função local;
- uma função local conter um corpo (body) com um bloco, ou com uma expressão, assim assim como em uma função não local;
- criar uma função local genérica e opcionalmente restringir os tipos genéricos, assim como em uma função não local;
- usar variáveis que estejam no escopo da função na qual a função local está contida, by ref, sem cópia alguma;
- usar os parâmetros genéricos que estejam no escopo da função na qual a função local está contida;
- usar parâmetros ref e out em funções locais.
Não será possível fazer qualquer controle de fluxo no corpo de uma função local, com relação à execução da função na qual ela está contida, ou seja, não será possível chamar yield, break, goto, continue, etc, de forma que fosse alterado o fluxo da função externa.
Um ponto a ser considerado é que praticamente já temos funções locais, já que delegates são exatamente isso. Isso aqui é um código C# totalmente válido:
void M() { Func<int, int, int> soma = (a, b) => a + b; var total = soma(1, 2); }
Porque local functions são uma boa ideia
Há diversos pontos em que funções locais são mais poderosas e interessantes que delegates e lambdas. Entre eles:
- Sintaxe consistente com a já utilizada em métodos;
- Não há necessidade de criar um delegate, ou referenciar Func, Action, ou algo parecido;
- Lambdas e delegates causam alocações extras, funções locais não;
- Ref e out são permitidos;
- Tipos genéricos são permitidos;
- É possível referenciar funções ainda não declaradas.
Esse código, por exemplo, não é possível, já que “soma” é declarado após “subtrai”:
public void M() { Func<int, int, int> subtrai = (a, b) => soma(a, -b); Func<int, int, int> soma = (a, b) => a + b; var total = subtrai(1, 2); }
A alocação extra também é fácil de ver. Todo delegate na verdade acaba gerando uma classe que não vemos, e que possui as variáveis que são usados dentro do método em si. É assim que as closures são criadas. Esse método, por exemplo:
public void M() { var quatro = DateTime.Now.Second; Func<int, int> soma4 = (a) => a + quatro; var total = soma4(1); }
Gera dois objetos na chamada, somente para que o método “soma4” possa ser chamado, e gera cópia da variável “quatro”. Vejam na IL gerada (descompilado com ILSpy):
.method public hidebysig instance void M () cil managed { // Method begins at RVA 0x2050 // Code size 48 (0x30) .maxstack 2 .locals init ( [0] class Foo/'<>c__DisplayClass0_0', [1] class [mscorlib]System.Func`2<int32, int32>, [2] int32, [3] valuetype [mscorlib]System.DateTime ) IL_0000: newobj instance void Foo/'<>c__DisplayClass0_0'::.ctor() IL_0005: stloc.0 IL_0006: nop IL_0007: ldloc.0 IL_0008: call valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now() IL_000d: stloc.3 IL_000e: ldloca.s 3 IL_0010: call instance int32 [mscorlib]System.DateTime::get_Second() IL_0015: stfld int32 Foo/'<>c__DisplayClass0_0'::quatro IL_001a: ldloc.0 IL_001b: ldftn instance int32 Foo/'<>c__DisplayClass0_0'::'<M>b__0'(int32) IL_0021: newobj instance void class [mscorlib]System.Func`2<int32, int32>::.ctor(object, native int) IL_0026: stloc.1 IL_0027: ldloc.1 IL_0028: ldc.i4.1 IL_0029: callvirt instance !1 class [mscorlib]System.Func`2<int32, int32>::Invoke(!0) IL_002e: stloc.2 IL_002f: ret } // end of method Foo::M
Note a criação da classe interna em IL_0000 (primeira alocação desnecessária), a cópia do campo de segundo em IL_0015, o método “soma4” em si, a soma de fato, que foi parar na class interna (de nome “Foo/<>c__DisplayClass0_0”), tem seu ponteiro carregado na memória em IL_001b, que é então passado para o construtor de um Func<int, int> em IL_0021 (segunda alocação desnecessária), e só é então chamado em IL_0029.
Essas duas alocações (da classe interna e de Func) serão evitadas com funções locais.
Resumindo
Funções locais são mais uma adição bem vinda, que traz ainda mais expressividade pro C#, e que estará no C# 7.
O que vocês acharam? Gostaram? Ficou muito parecido com JavaScript e isso é bom? Ficou muito parecido com JavaScript e isso é ruim? Dêem suas opiniões aqui nos comentários.
E temos tido boas conversas no Brasil.NET, vem conversar com a gente na sala de C# lá (se ainda não entrou, ou link pra entrar é esse: http://brasildotnet.azurewebsites.net/).
Giovanni Bassi
Arquiteto e desenvolvedor, agilista, escalador, provocador. É fundador e CSA da Lambda3. Programa porque gosta. Acredita que pessoas autogerenciadas funcionam melhor e por acreditar que heterarquia é mais eficiente que hierarquia. Foi reconhecido Microsoft MVP há mais de dez anos, dos mais de vinte que atua no mercado. Já palestrou sobre .NET, Rust, microsserviços, JavaScript, TypeScript, Ruby, Node.js, Frontend e Backend, Agile, etc, no Brasil, e no exterior. Liderou grupos de usuários em assuntos como arquitetura de software, Docker, e .NET.