Cosa sono le “chiusure” in .NET?

Cos’è una chiusura ? Li abbiamo in .NET?

Se esistono in .NET, potresti fornire uno snippet di codice (preferibilmente in C #) per spiegarlo?

EDIT: Ho letto l’articolo di Jon Skeet per capire quali sono le chiusure e come usarle in .NET.

Ho un articolo su questo argomento . (Ha molti esempi).

In sostanza, una chiusura è un blocco di codice che può essere eseguito in un secondo momento, ma che mantiene l’ambiente in cui è stato creato, cioè può ancora utilizzare le variabili locali ecc del metodo che lo ha creato, anche dopo il metodo ha terminato l’esecuzione.

La caratteristica generale delle chiusure è implementata in C # da metodi anonimi e espressioni lambda.

Ecco un esempio utilizzando un metodo anonimo:

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); }; } } 

Produzione:

 counter=1 counter=2 

Qui possiamo vedere che l’azione restituita da CreateAction ha ancora accesso alla variabile contatore e può effettivamente incrementarla, anche se CreateAction stesso ha finito.

Se sei interessato a vedere come C # implementa Closure leggi “Conosco la risposta (il suo 42) blog”

Il compilatore genera una class in background per incapsulare il metodo anonimo e la variabile j

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

per la funzione:

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

Trasformandolo in:

 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.b__0); } } 

Le chiusure sono valori funzionali che mantengono valori variabili dal loro ambito originale. C # può usarli sotto forma di delegati anonimi.

Per un esempio molto semplice, prendi questo codice C #:

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

Alla fine, la barra sarà impostata su 4 e il delegato myClosure può essere passato in giro per essere utilizzato altrove nel programma.

Le chiusure possono essere utilizzate per un sacco di cose utili, come l’esecuzione ritardata o per semplificare le interfacce – LINQ è costruito principalmente usando chiusure. Il modo più immediato per la maggior parte degli sviluppatori è l’aggiunta di gestori di eventi ai controlli creati dynamicmente: è ansible utilizzare le chiusure per aggiungere comportamenti quando il controllo viene istanziato, anziché archiviare i dati altrove.

 Func 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 

Una chiusura è una funzione anonima passata al di fuori della funzione in cui è stata creata. Mantiene qualsiasi variabile dalla funzione in cui viene creata che utilizza.

Ecco un esempio forzato di C # che ho creato da codice simile in JavaScript:

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

Quindi, ecco un codice che mostra come usare il codice sopra ...

 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 

Spero che sia alquanto utile.

Fondamentalmente la chiusura è un blocco di codice che puoi passare come argomento a una funzione. C # supporta le chiusure sotto forma di delegati anonimi.

Qui c’è un semplice esempio:
List.Find metodo può accettare ed eseguire pezzo di codice (chiusura) per trovare l’elemento della lista.

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

Usando la syntax C # 3.0 possiamo scrivere come:

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

Una chiusura è quando una funzione è definita all’interno di un’altra funzione (o metodo) e utilizza le variabili dal metodo genitore . Questo uso di variabili che si trovano in un metodo e avvolto in una funzione definita al suo interno, è chiamato chiusura.

Mark Seemann ha alcuni interessanti esempi di chiusure nel suo post sul blog in cui fa un parallelo tra oop e programmazione funzionale.

E per renderlo più dettagliato

 var workingDirectory = new DirectoryInfo(Environment.CurrentDirectory);//when this variable Func 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. 

Le chiusure sono blocchi di codice che fanno riferimento a una variabile al di fuori di se stessi (da sotto in pila), che potrebbe essere chiamata o eseguita successivamente (come quando un evento o un delegato è definito e potrebbe essere richiamato in un punto indefinito nel tempo futuro) ) … Poiché la variabile esterna che il blocco dei riferimenti al codice potrebbe uscire dall’ambito di applicazione (e sarebbe altrimenti andata persa), il fatto che sia referenziato dal blocco di codice (chiamato chiusura) indica al runtime di “tenere “quella variabile nel campo di applicazione fino a quando non è più necessaria dal blocco di chiusura del codice …

Ho cercato di capirlo anch’io, ben sotto ci sono i frammenti di codice per Same Code in Javascript e C # che mostra chiusura.

  1. Conta numero di volte in cui ogni evento è accaduto o no di quando si fa clic su ciascun pulsante.

JavaScript:

 var c = function () { var d = 0; function inner() { d++; alert(d); } return inner; }; var a = c(); var b = c();     

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. conteggio Numero totale di volte in cui è stato eseguito l’evento o conteggia il numero totale di clic indipendentemente dal controllo.

JavaScript:

 var c = function () { var d = 0; function inner() { d++; alert(d); } return inner; }; var a = c();   

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); } } 

Appena fuori dal nulla, una risposta semplice e più comprensiva dal libro C # 7.0 in poche parole.

Pre-requisito che dovresti sapere : Un’espressione lambda può fare riferimento alle variabili locali e ai parametri del metodo in cui è definito (variabili esterne).

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

Parte reale : le variabili esterne a cui fa riferimento un’espressione lambda sono chiamate variabili catturate. Un’espressione lambda che cattura le variabili è chiamata chiusura.

Ultimo punto da notare : le variabili catturate vengono valutate quando il delegato viene effettivamente invocato, non quando le variabili sono state acquisite:

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

Se si scrive un metodo anonimo in linea (C # 2) o (preferibilmente) un’espressione Lambda (C # 3 +), viene ancora creato un metodo effettivo. Se quel codice utilizza una variabile locale di ambito esterno, è comunque necessario passare in qualche modo quella variabile al metodo.

ad esempio, prendi questa clausola di Linq Where (che è un semplice metodo di estensione che passa un’espressione lambda):

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

se vuoi usare i in quell’espressione lambda, devi passarlo a quel metodo creato.

Quindi la prima domanda che sorge è: dovrebbe essere passato per valore o riferimento?

Passare per riferimento è (presumo) più preferibile in quanto si ottiene l’accesso in lettura / scrittura a tale variabile (e questo è ciò che fa C #: immagino che la squadra di Microsoft abbia pesato i pro ei contro e sia andata avanti con riferimento; secondo Jon Skeet articolo , Java è andato con il valore-by).

Ma poi sorge un’altra domanda: dove allocarlo?

Dovrebbe effettivamente / naturalmente essere assegnato in pila? Bene, se lo si assegna nello stack e lo si passa per riferimento, ci possono essere situazioni in cui sopravvive al proprio stack frame. Prendi questo esempio:

 static void Main(string[] args) { Outlive(); var list = whereItems.ToList(); Console.ReadLine(); } static IEnumerable whereItems; static void Outlive() { var i = 0; var items = new List { "Hello","World" }; whereItems = items.Where(x => { i++; Console.WriteLine(i); return true; }); } 

L’espressione lambda (nella clausola Where) crea nuovamente un metodo che fa riferimento a un i. Se i viene allocato nello stack di Outlive, quindi quando si enumerano i whereItem, l’i utilizzato nel metodo generato punterà all’i di Outlive, cioè a un punto nello stack che non è più accessibile.

Ok, quindi ne abbiamo bisogno in seguito.

Quindi, cosa fa il compilatore C # per supportare questo inline anonymous / lambda, si usa quello che viene chiamato ” Closures “: crea una class sull’Heap chiamato ( piuttosto male ) DisplayClass che ha un campo che contiene l’io e la funzione che effettivamente usa esso.

Qualcosa che sarebbe equivalente a questo (puoi vedere l’IL generato usando ILSpy o ILDASM):

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

Crea un’istanza di quella class nel tuo ambito locale e sostituisce qualsiasi codice relativo a i o all’espressione lambda con quell’istanza di chiusura. Quindi, ogni volta che si utilizza l’i nel codice “scope locale” in cui sono stato definito, si sta effettivamente utilizzando il campo dell’istanza DisplayClass.

Quindi, se dovessi cambiare l’i “locale” nel metodo principale, cambierà effettivamente _DisplayClass.i;

vale a dire

 var i = 0; var items = new List { "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 

stamperà 12, poiché “i = 10” va a quel campo dispalyclass e lo cambia appena prima della seconda enumerazione.

Una buona fonte sull’argomento è questo modulo Bart De Smet Pluralsight (richiede la registrazione) (ignora anche il suo uso erroneo del termine “Hoisting” – cosa (penso) lui vuol dire che la variabile locale (ie i) è cambiata per riferirsi al nuovo campo DisplayClass).


In altre notizie, sembra esserci qualche equivoco sul fatto che le “chiusure” siano legate ai loop – come capisco che le “chiusure” NON sono un concetto legato ai loop , ma piuttosto ai metodi anonimi / espressioni lambda che usano le variabili locali dell’ambito – sebbene qualche trucco le domande usano i loop per dimostrarlo.

Una chiusura è una funzione, definita all’interno di una funzione, che può accedere alle sue variabili locali e al suo genitore.

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

quindi la funzione all’interno del metodo di ricerca.

  t => t.Name == name 

può accedere alle variabili all’interno del suo ambito, t, e il nome della variabile che è nel suo ambito genitori. Anche se viene eseguito dal metodo find come delegato, da un altro ambito tutti insieme.