Novidades do C# 7: Local Functions | Lambda3
Novidades do C# 7: Local Functions
por Giovanni Bassi | abr 11, 2016 | Microsoft | 9 Comentários
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:
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:
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:
Esse código, por exemplo, não é possível, já que "soma" é declarado após "subtrai":
Func<int, int, int> subtrai = (a, b) => soma(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:
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):
instance void M () cil managed
[0] class Foo/'<>c__DisplayClass0_0',
[1] class [mscorlib]System.Func`2<int32, int32>,
[3] valuetype [mscorlib]System.DateTime
IL_0000: newobj instance void Foo/'<>c__DisplayClass0_0'::.ctor()
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_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_0028: ldc.i4.1
IL_0029: callvirt instance !1 class [mscorlib]System.Func`2<int32, int32>::Invoke(!0)
} // 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.
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.