Back-End

25 nov, 2016

Apresentando o conceito de Closures

Publicidade

Neste artigo vou apresentar o conceito de Closures e seu uso na linguagem C#.

O que são Closures?

Closures (clausura, em português) é um conceito geralmente associado com as linguagens de programação funcionais (JavaScript, F# etc.) que vinculam uma função ao seu ambiente de referência, permitindo o acesso a variáveis fora do escopo da função.

Na linguagem C# podemos usar Closures usando delegates.

A linguagem JavaScript usa muito esse recurso e o conceito de closure usado aqui foi obtido no site JavaScript Brasil (que parece não estar mais atendendo).

Um closure é uma função interior que tem acesso a variáveis de uma função exterior – cadeia de escopo.

O closure tem três cadeias de escopo:

  1. O seu próprio escopo;
  2. O escopo externo, tendo acesso as variáveis da função exterior;
  3. E o acesso as variáveis globais.

A função interior tem acesso não somente as variáveis da função exterior, mas também aos parâmetros dela.

Observe que a função interior não pode chamar o objeto (argumentos) da função exterior, entretanto, pode chamar parâmetros da função externa diretamente.

Abaixo, vemos um exemplo de Closure usando JavaScript (o exemplo abaixo usa o Nodejs):

var contar = (function () {
    var contador = 0;
    return function () 
     { 
        return contador += 1;        
     }
})();

c_closu1

Para testar a função, invocamos variável contar() 3 vezes e notamos que o contador foi incrementado.

A variável contar é atribuída ao valor de retorno de uma função que é auto invocada.

A função auto invocada só é executada uma vez. Ela define o contador igual a zero(0), e retorna uma expressão de função. Desta forma, a variável contar torna-se uma função. O pulo do gato é que ela pode acessar o contador no escopo pai. Isso torna possível para uma função ter variáveis ‘privadas’.

O contador é protegido pelo escopo da função anônima, e só pode ser alterado usando a função contar.

Bem, tudo isso para mostrar o conceito de Closure. Mas onde entra a linguagem C# nessa história?

Usando Closures com C#

Na linguagem C#, closures podem ser criadas usando métodos anônimos ou expressões lambdas (vai depender da sua versão do .NET Framework).

Quando você cria uma função, as variáveis que ela vai usar e que estão fora do escopo de visibilidade, são copiadas e armazenadas com o código da closure. Assim, elas podem ser usadas sempre que o delegate for chamado.

Isso nos traz uma grande flexibilidade quando usamos delegates, mas também introduz a possibilidade e erros inesperados.

Vamos, então, iniciar com um exemplo bem simples criado no VS 2015 Community usando uma aplicação Console Application:

class Program
    {
        static void Main(string[] args)
        {
            int naoLocal = 1;
            Action closure = delegate
            {
                Console.WriteLine("{0} + 1 = {1}", naoLocal, naoLocal + 1);
            };
            closure();
            Console.ReadKey();
        }
    }

c_closu2

Neste código criamos um variável chamada naoLocal a atribuímos o valor inteiro 1.

A segunda instrução cria uma instância do delegate Action, o qual gera uma mensagem que usa o valor da variável naoLocal.

Depois chamamos o delegate closure() para ver a mensagem.

O resultado é visto na figura ao lado.

Podemos fazer a mesma coisa usando expressões lambdas:

 static void Main(string[] args)
    {
            int naoLocal = 1;
            Action closure = () =>
            {
                Console.WriteLine("{0} + 1 = {1}", naoLocal, naoLocal + 1);
            };
            closure();
            Console.ReadKey();
     }

Nada muito diferente do que você poderia esperar. Não é mesmo?

Vejamos, então, outro exemplo um pouco mais elaborado.

class Program
{
        static Action closure;
        static void Main(string[] args)
        {
            DefineClosure();
            closure();
            Console.ReadKey();
        }
        private static void DefineClosure()
        {
            int naoLocal = 1;
            closure = () =>
            {
                Console.WriteLine("{0} + 1 = {1}", naoLocal, naoLocal + 1);
            };
        }
}

Neste exemplo, o closure está em uma variável Action a nível de classe.

O método Main() chama o método DefineClosure() para inicializar a closure antes de executá-la.

Você pode ver que a variável inteira é criada e inicializada e, então, usada dentro da closure.

Após a conclusão do método DefineClosure, esta variável inteira sai do escopo.

No entanto, ainda estamos invocando o delegado depois que isso acontecer.

Agora, o resultado não é tão óbvio.

Será que vai compilar e executar corretamente? Executando o código você obterá o mesmo resultado do exemplo anterior. Isso é o Closure em ação.

A variável naoLocal foi capturada pelo código do delegate, fazendo com que ela permaneça no escopo além dos limites normais.

Na verdade, ela permanecerá disponível até que as outras referências ao delegate permaneçam.

Funcionou, mas na verdade os Closures não têm um suporte no .NET Framework.

O que realmente acontece é que nos bastidores ocorre o trabalho do compilador.

Quando você constrói o seu projeto, o compilador gera uma nova classe oculta, que encapsula as variáveis não locais e o código que você inclui no método ou expressão lambda anônimo.

O código está incluído em um método e as variáveis locais são representados como campos.

Este novo método da classe é chamado quando o delegate for executado.

Nota: A linguagem C# captura as próprias variáveis usadas nas closures e não o seu valor.

Até o próximo artigo sobre C#!