O que são 'fechamentos' no .NET?


195

O que é um fechamento ? Nós os temos no .NET?

Se eles existirem no .NET, você poderia fornecer um trecho de código (de preferência em C #) explicando-o?

Respostas:


258

Eu tenho um artigo sobre esse mesmo tópico . (Ele tem muitos exemplos.)

Em essência, um fechamento é um bloco de código que pode ser executado posteriormente, mas que mantém o ambiente em que foi criado - ou seja, ele ainda pode usar as variáveis ​​locais etc. do método que o criou, mesmo depois disso. O método concluiu a execução.

O recurso geral de fechamentos é implementado em C # por métodos anônimos e expressões lambda.

Aqui está um exemplo usando um método anônimo:

using System;

class Test
{
    static void Main()
    {
        Action action = CreateAction();
        action();
        action();
    }

    static Action CreateAction()
    {
        int counter = 0;
        return delegate
        {
            // Yes, it could be done in one statement; 
            // but it is clearer like this.
            counter++;
            Console.WriteLine("counter={0}", counter);
        };
    }
}

Resultado:

counter=1
counter=2

Aqui podemos ver que a ação retornada por CreateAction ainda tem acesso à variável counter e pode realmente incrementá-la, mesmo que o próprio CreateAction tenha terminado.


57
Obrigado Jon. BTW, há algo que você não conhece no .NET? :) A quem você vai quando tem dúvidas?
Desenvolvedor

44
Há sempre mais para aprender :) Acabei de ler o CLR via C # - muito informativo. Fora isso, normalmente pergunto a Marc Gravell sobre árvores de ligação / expressão de WCF e a Eric Lippert sobre coisas da linguagem C #.
9139 Jon Skeet

2
Notei isso, mas ainda acho que sua afirmação de que "é um bloco de código que pode ser executado posteriormente" é simplesmente incorreta - não tem nada a ver com execução, tem mais a ver com valores de variáveis ​​e com o escopo do que com a execução , por si só.
9139 Jason Bunting

11
Eu diria que os fechamentos não são úteis a menos que possam ser executados, e o "mais tarde" destaca a "estranheza" de poder capturar o ambiente (que de outra forma poderia ter desaparecido pelo tempo de execução). Se você citar apenas metade da frase, é uma resposta incompleta, é claro.
Jon Skeet

4
@SLC: Sim, counterestá disponível para ser incrementado - o compilador gera uma classe que contém um countercampo e qualquer código referente a ele counteracaba passando por uma instância dessa classe.
Jon Skeet

22

Se você estiver interessado em ver como o C # implementa o Closure, leia "Conheço a resposta (seu 42) blog"

O compilador gera uma classe em segundo plano para encapsular o método anoymous e a variável j

[CompilerGenerated]
private sealed class <>c__DisplayClass2
{
    public <>c__DisplayClass2();
    public void <fillFunc>b__0()
    {
       Console.Write("{0} ", this.j);
    }
    public int j;
}

para a função:

static void fillFunc(int count) {
    for (int i = 0; i < count; i++)
    {
        int j = i;
        funcArr[i] = delegate()
                     {
                         Console.Write("{0} ", j);
                     };
    } 
}

Transformando-o em:

private static void fillFunc(int count)
{
    for (int i = 0; i < count; i++)
    {
        Program.<>c__DisplayClass1 class1 = new Program.<>c__DisplayClass1();
        class1.j = i;
        Program.funcArr[i] = new Func(class1.<fillFunc>b__0);
    }
}

Oi Daniil - Você responde é muito útil e eu queria ir além da sua resposta e acompanhar, mas o link está quebrado. Infelizmente, meu googlefu não é bom o suficiente para descobrir para onde foi movido.
Knox

10

Encerramentos são valores funcionais que mantêm valores variáveis ​​de seu escopo original. O C # pode usá-los na forma de delegados anônimos.

Para um exemplo muito simples, use este código C #:

    delegate int testDel();

    static void Main(string[] args)
    {
        int foo = 4;
        testDel myClosure = delegate()
        {
            return foo;
        };
        int bar = myClosure();

    }

No final, bar será definido como 4, e o delegado myClosure poderá ser distribuído para ser usado em outras partes do programa.

Os closures podem ser usados ​​para muitas coisas úteis, como execução atrasada ou para simplificar interfaces - o LINQ é construído principalmente usando closures. A maneira mais imediata de ser útil para a maioria dos desenvolvedores é adicionar manipuladores de eventos a controles criados dinamicamente - você pode usar fechamentos para adicionar comportamento quando o controle é instanciado, em vez de armazenar dados em outro local.


10
Func<int, int> GetMultiplier(int a)
{
     return delegate(int b) { return a * b; } ;
}
//...
var fn2 = GetMultiplier(2);
var fn3 = GetMultiplier(3);
Console.WriteLine(fn2(2));  //outputs 4
Console.WriteLine(fn2(3));  //outputs 6
Console.WriteLine(fn3(2));  //outputs 6
Console.WriteLine(fn3(3));  //outputs 9

Um fechamento é uma função anônima passada fora da função em que é criada. Ele mantém quaisquer variáveis ​​da função em que é criado e que usa.


4

Aqui está um exemplo artificial para C # que eu criei a partir de código semelhante em JavaScript:

public delegate T Iterator<T>() where T : class;

public Iterator<T> CreateIterator<T>(IList<T> x) where T : class
{
        var i = 0; 
        return delegate { return (i < x.Count) ? x[i++] : null; };
}

Então, aqui está um código que mostra como usar o código acima ...

var iterator = CreateIterator(new string[3] { "Foo", "Bar", "Baz"});

// So, although CreateIterator() has been called and returned, the variable 
// "i" within CreateIterator() will live on because of a closure created 
// within that method, so that every time the anonymous delegate returned 
// from it is called (by calling iterator()) it's value will increment.

string currentString;    
currentString = iterator(); // currentString is now "Foo"
currentString = iterator(); // currentString is now "Bar"
currentString = iterator(); // currentString is now "Baz"
currentString = iterator(); // currentString is now null

Espero que seja um pouco útil.


1
Você deu um exemplo, mas não ofereceu uma definição geral. Eu deduzo dos seus comentários aqui que eles são "mais sobre o escopo", mas certamente há mais do que isso?
ladenedge 23/09/10

2

Basicamente, o fechamento é um bloco de código que você pode transmitir como argumento para uma função. O C # suporta fechamentos na forma de delegados anônimos.

Aqui está um exemplo simples: o
método List.Find pode aceitar e executar trechos de código (fechamento) para encontrar o item da lista.

// Passing a block of code as a function argument
List<int> ints = new List<int> {1, 2, 3};
ints.Find(delegate(int value) { return value == 1; });

Usando a sintaxe C # 3.0, podemos escrever isso como:

ints.Find(value => value == 1);

1
Detesto ser técnico, mas o fechamento tem mais a ver com o escopo - um fechamento pode ser criado de várias maneiras diferentes, mas o fechamento não é o meio, é o fim.
9139 Jason Bunting

2

Um fechamento é quando uma função é definida dentro de outra função (ou método) e usa as variáveis ​​do método pai . Esse uso de variáveis ​​localizadas em um método e envolvidas em uma função definida dentro dele é chamado de fechamento.

Mark Seemann tem alguns exemplos interessantes de fechamentos em seu blog, onde ele faz um paralelo entre oop e programação funcional.

E para torná-lo mais detalhado

var workingDirectory = new DirectoryInfo(Environment.CurrentDirectory);//when this variable
Func<int, string> read = id =>
    {
        var path = Path.Combine(workingDirectory.FullName, id + ".txt");//is used inside this function
        return File.ReadAllText(path);
    };//the entire process is called a closure.

1

Closures são blocos de código que referenciam uma variável fora de si mesmos (abaixo deles na pilha), que podem ser chamados ou executados mais tarde (como quando um evento ou delegado é definido e podem ser chamados em algum momento futuro indefinido) ) ... Como a variável externa à qual o pedaço de código faz referência pode ficar fora de escopo (e de outra forma teria sido perdida), o fato de ser referenciada pelo pedaço de código (chamado de fechamento) diz ao tempo de execução para "manter "essa variável no escopo até que não seja mais necessária pelo pedaço de código de fechamento ...


Como indiquei na explicação de outra pessoa: odeio ser técnico, mas o fechamento tem mais a ver com o escopo - um fechamento pode ser criado de várias maneiras diferentes, mas o fechamento não é o meio, é o fim.
21139 Jason Bunting

1
Os fechamentos são relativamente novos para mim, então é perfeitamente possível que eu entenda mal, mas entendo a parte do escopo. Minha resposta está focada no escopo. Então, estou perdendo o que o comentário do ano está tentando corrigir ... O que mais o escopo pode ser relevante para além de algum pedaço de código? (função, método anônimo, ou qualquer outro)
Charles Bretana

Não é a chave para o fechamento de que algum "pedaço de código executável" pode acessar um valor de variável ou de memória que sintaticamente "fora" de seu escopo, depois que essa variável deve ter ficado "fora de escopo" ou ser destruída normalmente ?
22816 Charles Bretana

@ Jason, não se preocupe em ser técnico, essa ideia de fechamento é algo que demorei um pouco para entender, em longas discussões com um colega de trabalho, sobre fechamentos de javascript ... mas ele era louco por Lisp e eu nunca tenho muito através das abstrações em suas explicações ...
Charles Bretana

0

Eu tenho tentado entender também, bem abaixo estão os trechos de código para o mesmo código em Javascript e C # mostrando o fechamento.

  1. Número de Contagem de Vezes em que cada evento ocorreu ou não de cliques em cada botão.

JavaScript:

var c = function ()
{
    var d = 0;

    function inner() {
      d++;
      alert(d);
  }

  return inner;
};

var a = c();
var b = c();

<body>
<input type=button value=call onClick="a()"/>
  <input type=button value=call onClick="b()"/>
</body>

C #:

using System.IO;
using System;

class Program
{
    static void Main()
    {
      var a = new a();
      var b = new a();

       a.call();
       a.call();
       a.call();

       b.call();
       b.call();
       b.call();
    }
}

public class a {

    int b = 0;

    public  void call()
    {
      b++;
     Console.WriteLine(b);
    }
}
  1. count Número total de vezes que o evento de clique ocorreu ou número total de cliques, independentemente do controle.

JavaScript:

var c = function ()
{
    var d = 0;

    function inner() {
     d++;
     alert(d);
  }

  return inner;
};

var a = c();

<input type=button value=call onClick="a()"/>
  <input type=button value=call onClick="a()"/>

C #:

using System.IO;
using System;

class Program
{
    static void Main()
    {
      var a = new a();
      var b = new a();

       a.call();
       a.call();
       a.call();

       b.call();
       b.call();
       b.call();
    }
}

public class a {

    static int b = 0;

    public void call()
    {
      b++;
     Console.WriteLine(b);
    }
}

0

Do nada, uma resposta simples e mais compreensiva do resumo nutricional do livro C # 7.0.

Pré-requisito que você deve saber : Uma expressão lambda pode fazer referência às variáveis ​​e parâmetros locais do método em que está definida (variáveis ​​externas).

    static void Main()
    {
    int factor = 2;
   //Here factor is the variable that takes part in lambda expression.
    Func<int, int> multiplier = n => n * factor;
    Console.WriteLine (multiplier (3)); // 6
    }

Parte real : variáveis ​​externas referenciadas por uma expressão lambda são chamadas de variáveis ​​capturadas. Uma expressão lambda que captura variáveis ​​é chamada de fechamento.

Último ponto a ser observado : As variáveis ​​capturadas são avaliadas quando o delegado é realmente chamado, não quando as variáveis ​​foram capturadas:

int factor = 2;
Func<int, int> multiplier = n => n * factor;
factor = 10;
Console.WriteLine (multiplier (3)); // 30

0

Se você escrever um método anônimo em linha (C # 2) ou (preferencialmente) uma expressão Lambda (C # 3 +), um método real ainda estará sendo criado. Se esse código estiver usando uma variável local de escopo externo - você ainda precisará passar essa variável para o método de alguma forma.

por exemplo, use esta cláusula Linq Where (que é um método de extensão simples que transmite uma expressão lambda):

var i = 0;
var items = new List<string>
{
    "Hello","World"
};   
var filtered = items.Where(x =>
// this is a predicate, i.e. a Func<T, bool> written as a lambda expression
// which is still a method actually being created for you in compile time 
{
    i++;
    return true;
});

se você quiser usar i nessa expressão lambda, precisará passá-la para o método criado.

Portanto, a primeira pergunta que se coloca é: deve ser passada por valor ou referência?

Passar por referência é (eu acho) mais preferível quando você obtém acesso de leitura / gravação a essa variável (e é isso que o C # faz; acho que a equipe da Microsoft pesou os prós e contras e seguiu com referência); de acordo com Jon Skeet artigo , Java foi com valor).

Mas então surge outra pergunta: onde alocar esse i?

Deveria realmente / naturalmente ser alocado na pilha? Bem, se você alocá-lo na pilha e passá-lo por referência, pode haver situações em que ela sobrevive ao seu próprio quadro de pilha. Veja este exemplo:

static void Main(string[] args)
{
    Outlive();
    var list = whereItems.ToList();
    Console.ReadLine();
}

static IEnumerable<string> whereItems;

static void Outlive()
{
    var i = 0;
    var items = new List<string>
    {
        "Hello","World"
    };            
    whereItems = items.Where(x =>
    {
        i++;
        Console.WriteLine(i);
        return true;
    });            
}

A expressão lambda (na cláusula Where) cria novamente um método que se refere a um i. Se i estiver alocado na pilha do Outlive, quando você enumerar os whereItems, o i usado no método gerado apontará para o i do Outlive, ou seja, para um local na pilha que não está mais acessível.

Ok, então precisamos disso na pilha então.

Então, o que o compilador C # faz para oferecer suporte a esse anônimo / lambda embutido é usar o que é chamado " Closures ": Ele cria uma classe no Heap chamada ( bastante ) DisplayClass que possui um campo que contém i e a Function que realmente usa isto.

Algo que seria equivalente a isso (você pode ver a IL gerada usando ILSpy ou ILDASM):

class <>c_DisplayClass1
{
    public int i;

    public bool <GetFunc>b__0()
    {
        this.i++;
        Console.WriteLine(i);
        return true;
    }
}

Instancia essa classe no seu escopo local e substitui qualquer código relacionado a i ou à expressão lambda por essa instância de fechamento. Então - sempre que você estiver usando o i no seu código de "escopo local" onde eu fui definido, você estará realmente usando esse campo de instância DisplayClass.

Portanto, se eu mudasse o "local" i no método principal, ele realmente mudará _DisplayClass.i;

ie

var i = 0;
var items = new List<string>
{
    "Hello","World"
};  
var filtered = items.Where(x =>
{
    i++;
    return true;
});
filtered.ToList(); // will enumerate filtered, i = 2
i = 10;            // i will be overwriten with 10
filtered.ToList(); // will enumerate filtered again, i = 12
Console.WriteLine(i); // should print out 12

ele imprimirá 12, quando "i = 10" for para esse campo de distribuição e o alterar imediatamente antes da segunda enumeração.

Uma boa fonte sobre o assunto é este módulo Bart De Smet Pluralsight (requer registro) (também ignora seu uso incorreto do termo "Elevação" - o que (eu acho) ele quer dizer é que a variável local (ie i) é alterada para se referir para o novo campo DisplayClass).


Em outras notícias, parece haver alguma concepção errônea de que "Closures" estão relacionados a loops - como eu entendo "Closures" NÃO são um conceito relacionado a loops , mas sim a métodos anônimos / expressões lambda que usam variáveis ​​locais com escopo - embora alguns truques as perguntas usam loops para demonstrá-lo.


-1

Um fechamento é uma função, definida dentro de uma função, que pode acessar as variáveis ​​locais e seu pai.

public string GetByName(string name)
{
   List<things> theThings = new List<things>();
  return  theThings.Find<things>(t => t.Name == name)[0];
}

então a função dentro do método find.

 t => t.Name == name

pode acessar as variáveis ​​dentro de seu escopo, t, e o nome da variável que está no escopo de seus pais. Mesmo que seja executado pelo método find como um delegado, de outro escopo todos juntos.


2
Um fechamento não é uma função, por si só, é mais definido falando sobre escopo do que funções. As funções meramente auxiliam na manutenção do escopo, o que causa a criação de um fechamento. Mas dizer que um fechamento é uma função não é tecnicamente correto. Desculpe nitpick. :)
Jason Bunting
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.