Co to są „zamknięcia” w .NET?

195

Co to jest zamknięcie ? Czy mamy je w .NET?

Jeśli istnieją w .NET, czy możesz podać fragment kodu (najlepiej w języku C #), który to wyjaśnia?

Deweloper
źródło

Odpowiedzi:

258

Mam artykuł na ten temat . (Ma wiele przykładów.)

Zasadniczo zamknięcie to blok kodu, który można wykonać w późniejszym czasie, ale który utrzymuje środowisko, w którym został utworzony po raz pierwszy - tzn. Może nadal używać zmiennych lokalnych itp. Metody, która go utworzyła, nawet po tym metoda zakończyła wykonywanie.

Ogólna funkcja zamknięć jest implementowana w języku C # za pomocą anonimowych metod i wyrażeń lambda.

Oto przykład z wykorzystaniem anonimowej metody:

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

Wynik:

counter=1
counter=2

Tutaj widzimy, że akcja zwrócona przez CreateAction nadal ma dostęp do zmiennej counter i może ją zwiększyć, nawet jeśli CreateAction zakończyło się.

Jon Skeet
źródło
57
Dzięki Jon. BTW, czy jest coś, czego nie znasz w .NET? :) Do kogo idziesz, gdy masz pytania?
Deweloper
44
Zawsze jest więcej do nauczenia :) Właśnie skończyłem czytać CLR przez C # - bardzo pouczające. Poza tym zwykle pytam Marc Gravell o drzewa WCF / wiążące / wyrażające, a Eric Lippert o rzeczy w języku C #.
Jon Skeet
2
Zauważyłem to, ale nadal uważam, że twoje stwierdzenie, że jest to „blok kodu, który można wykonać w późniejszym czasie”, jest po prostu niepoprawne - nie ma nic wspólnego z wykonaniem, bardziej dotyczy wartości zmiennych i zakresu niż wykonanie , jako taki.
Jason Bunting
11
Powiedziałbym, że zamknięcia nie są użyteczne, chyba że można je wykonać, a „w późniejszym czasie” podkreśla „dziwność” polegającą na możliwości przechwytywania środowiska (które w innym przypadku mogłyby zniknąć przed czasem wykonania). Cytując tylko połowę zdania, jest to oczywiście niepełna odpowiedź.
Jon Skeet
4
@SLC: Tak, countermożna zwiększać - kompilator generuje klasę zawierającą counterpole, a każdy kod odnoszący się do counterkończy się przechodzeniem przez instancję tej klasy.
Jon Skeet,
22

Jeśli chcesz zobaczyć, jak C # implementuje zamknięcie, przeczytaj: „Znam odpowiedź (jego 42) blog”

Kompilator generuje w tle klasę do enkapsulacji metody anoymicznej i zmiennej j

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

dla funkcji:

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

Przekształcając go w:

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);
    }
}
Daniil Grankin
źródło
Cześć, Daniil - Twoja odpowiedź jest bardzo przydatna i chciałem wyjść poza twoją odpowiedź i sprawdzić, ale link jest zepsuty. Niestety mój googlefu nie jest wystarczająco dobry, aby znaleźć miejsce, do którego się przeprowadził.
Knox
10

Zamknięcia to wartości funkcjonalne, które zachowują wartości zmienne z pierwotnego zakresu. C # może używać ich w formie anonimowych delegatów.

Na bardzo prosty przykład weź ten kod C #:

    delegate int testDel();

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

    }

Na końcu pasek zostanie ustawiony na 4, a delegat myClosure może zostać przekazany do użycia w innym miejscu programu.

Zamknięć można używać do wielu przydatnych rzeczy, takich jak opóźnione wykonanie lub w celu uproszczenia interfejsów - LINQ jest zbudowany głównie z zamknięć. Najbardziej bezpośrednim sposobem, w jaki przydaje się większość programistów, jest dodawanie procedur obsługi zdarzeń do dynamicznie tworzonych kontrolek - możesz użyć zamknięć, aby dodać zachowanie, gdy kontrola jest utworzona, zamiast przechowywać dane w innym miejscu.

Dan Monego
źródło
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

Zamknięcie to anonimowa funkcja przekazana poza funkcję, w której została utworzona. Zachowuje wszelkie zmienne z funkcji, w której zostało utworzone, z której korzysta.

AnthonyWJones
źródło
4

Oto wymyślony przykład dla C #, który utworzyłem z podobnego kodu w 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; };
}

Oto kod, który pokazuje, jak użyć powyższego kodu ...

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

Mam nadzieję, że jest to trochę pomocne.

Jason Bunting
źródło
1
Podałeś przykład, ale nie podałeś ogólnej definicji. Z twoich komentarzy wynika, że ​​są one „bardziej związane z zakresem”, ale na pewno jest coś więcej niż to?
ladenedge
2

Zasadniczo zamknięcie to blok kodu, który można przekazać jako argument funkcji. C # obsługuje zamknięcia w formie anonimowych delegatów.

Oto prosty przykład:
Metoda List.Find może zaakceptować i wykonać fragment kodu (zamknięcie) w celu znalezienia elementu listy.

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

Używając składni C # 3.0 możemy zapisać to jako:

ints.Find(value => value == 1);
aku
źródło
1
Nienawidzę być technicznym, ale zamknięcie ma więcej wspólnego z zakresem - zamknięcie można utworzyć na kilka różnych sposobów, ale zamknięcie nie jest środkiem, to koniec.
Jason Bunting
2

Zamknięcie ma miejsce, gdy funkcja jest zdefiniowana w innej funkcji (lub metodzie) i wykorzystuje zmienne z metody nadrzędnej . Takie użycie zmiennych, które są zlokalizowane w metodzie i opakowane w zdefiniowaną w niej funkcję, nazywa się zamknięciem.

Mark Seemann ma kilka interesujących przykładów zamknięć w swoim poście na blogu, w którym działa równolegle między programowaniem a programowaniem funkcjonalnym.

I żeby było bardziej szczegółowe

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.
meJustAndrew
źródło
1

Zamknięcia to fragmenty kodu, które odwołują się do zmiennej poza sobą (od dołu na stosie), które mogą zostać wywołane lub wykonane później (na przykład, gdy zdefiniowane jest zdarzenie lub delegat, i mogą zostać wywołane w nieokreślonym czasie w przyszłości ) ... Ponieważ zewnętrzna zmienna, do której fragment odwołania do kodu mógł wyjść poza zakres (i w przeciwnym razie zostałby utracony), fakt, że odwołuje się do niego fragment kodu (zwany zamknięciem), mówi środowisku wykonawczemu, aby „wstrzymał” „ta zmienna ma zasięg, dopóki nie będzie już potrzebna przez fragment kodu zamknięcia ...

Charles Bretana
źródło
Jak wskazałem na cudzym wyjaśnieniu: Nienawidzę być technicznym, ale zamknięcie ma więcej wspólnego z zakresem - zamknięcie można utworzyć na kilka różnych sposobów, ale zamknięcie nie jest środkiem, to koniec.
Jason Bunting
1
Zamknięcia są dla mnie stosunkowo nowe, więc jest całkiem możliwe, że źle zrozumiałem, ale dostaję część zakresu. Moja odpowiedź skupia się na zakresie. Brakuje mi więc tego, co twój komentarz próbuje poprawić ... Co jeszcze może mieć związek z istotnym fragmentem kodu? (funkcja, anonimowa metoda lub cokolwiek innego)
Charles Bretana
Nie jest kluczem do zamknięcia, że ​​jakaś „część kodu uruchamialnego” może uzyskać dostęp do wartości zmiennej lub wartości w pamięci, że syntaktycznie „poza” swoim zakresem, po tym, jak zmienna ta powinna zwykle „wyjść poza zakres” lub zostać zniszczona ?
Charles Bretana,
I @Jason, nie martw się o bycie technicznym, ten pomysł na zamknięcie jest czymś, co poświęciłem trochę czasu, aby otulić się głową, podczas długich dyskusji ze współpracownikiem, o zamknięciach javascript ... ale on był wariatem Lisp i nigdy nie całkiem wyszedł z abstrakcji w swoich wyjaśnieniach ...
Charles Bretana
0

Próbowałem to też zrozumieć, poniżej znajdują się fragmenty kodu dla tego samego kodu w JavaScript i C # pokazujące zamknięcie.

  1. Policz liczbę razy, kiedy zdarzenie miało miejsce lub liczbę kliknięć każdego przycisku.

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>

DO#:

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 Łączna liczba przypadków zdarzenia kliknięcia lub całkowita liczba kliknięć niezależnie od kontroli.

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()"/>

DO#:

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);
    }
}
Vimalshah26
źródło
0

Prosta i bardziej zrozumiała odpowiedź z książki w skrócie C # 7.0.

Wymagania wstępne, które powinieneś wiedzieć : Wyrażenie lambda może odwoływać się do zmiennych lokalnych i parametrów metody, w której jest zdefiniowane (zmienne zewnętrzne).

    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
    }

Część rzeczywista : Zmienne zewnętrzne, do których odwołuje się wyrażenie lambda, nazywane są zmiennymi przechwyconymi. Wyrażenie lambda, które przechwytuje zmienne, nazywa się zamknięciem.

Ostatni punkt, na który należy zwrócić uwagę : Przechwycone zmienne są oceniane podczas rzeczywistego wywołania delegata, a nie podczas przechwytywania zmiennych

int factor = 2;
Func<int, int> multiplier = n => n * factor;
factor = 10;
Console.WriteLine (multiplier (3)); // 30
Hameed Syed
źródło
0

Jeśli napiszesz wbudowaną anonimową metodę (C # 2) lub (najlepiej) wyrażenie Lambda (C # 3 +), nadal tworzona jest faktyczna metoda. Jeśli ten kod używa zmiennej lokalnej o zasięgu zewnętrznym - nadal musisz jakoś przekazać tę zmienną do metody.

np. weź tę klauzulę Linq Where (która jest prostą metodą rozszerzenia, która przekazuje wyrażenie 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;
});

jeśli chcesz użyć i w tym wyrażeniu lambda, musisz przekazać ją do utworzonej metody.

Pierwsze pytanie, które się pojawia, brzmi: czy powinno być przekazywane przez wartość czy odniesienie?

Przekazywanie przez referencję jest (jak sądzę) bardziej korzystne, gdy uzyskujesz dostęp do odczytu / zapisu do tej zmiennej (i to właśnie robi C #; Myślę, że zespół w Microsoft rozważał zalety i wady i poszedł z odniesieniem; Według Jona Skeeta artykuł , Java poszedł z wartościami).

Ale pojawia się kolejne pytanie: gdzie to przydzielić?

Czy powinien / powinien być faktycznie / naturalnie przydzielony na stosie? Cóż, jeśli przydzielisz go na stos i przekażesz przez referencję, mogą wystąpić sytuacje, w których przeżyje własną ramkę stosu. Weź ten przykład:

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

Wyrażenie lambda (w klauzuli Where) ponownie tworzy metodę, która odwołuje się do i. Jeśli i zostanie przydzielone na stosie Outlive, to do czasu wyliczenia whereItems, i użyte w wygenerowanej metodzie wskaże i na Outlive, tj. Miejsce na stosie, które nie jest już dostępne.

Ok, więc potrzebujemy go na stosie.

Więc to, co kompilator C # robi w celu obsługi tego wbudowanego anonimowego / lambda, polega na użyciu tak zwanych „ zamknięć ”: Tworzy klasę na stosie o nazwie ( raczej słabo ) DisplayClass, która zawiera pole zawierające i oraz funkcję, która faktycznie używa to.

Coś, co byłoby równoważne z tym (możesz zobaczyć IL wygenerowaną za pomocą ILSpy lub ILDASM):

class <>c_DisplayClass1
{
    public int i;

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

Tworzy instancję tej klasy w twoim zasięgu lokalnym i zastępuje każdy kod związany z wyrażeniem i lub lambda tą instancją zamknięcia. Tak więc - za każdym razem, gdy używasz „i” w kodzie „zasięgu lokalnego”, w którym został zdefiniowany, faktycznie używasz tego pola instancji DisplayClass.

Więc jeśli zmienię „lokalne” i w metodzie głównej, faktycznie zmieni ona _DisplayClass.i;

to znaczy

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

wypisze 12, ponieważ „i = 10” trafia do tego pola disalyclass i zmienia je tuż przed 2. wyliczeniem.

Dobrym źródłem na ten temat jest ten moduł Bart De Smet Pluralsight (wymaga rejestracji) (również zignoruj ​​jego błędne użycie terminu „Podnoszenie” - co (myślę) ma na myśli to, że zmienna lokalna (tj. I) została zmieniona tak, aby odwoływała się do nowego pola DisplayClass).


W innych wiadomościach wydaje się, że istnieje pewne błędne przekonanie, że „Zamknięcia” są powiązane z pętlami - jak rozumiem, „Zamknięcia” NIE są pojęciem związanym z pętlami , ale raczej z użyciem anonimowych metod / wyrażeń lambda z wykorzystaniem zmiennych lokalnych o zasięgu - chociaż pewne sztuczki pytania używają pętli, aby to wykazać.

David Refaeli
źródło
-1

Zamknięcie to funkcja zdefiniowana w funkcji, która może uzyskiwać dostęp do lokalnych zmiennych zarówno jej, jak i jej elementu nadrzędnego.

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

więc funkcja wewnątrz metody find.

 t => t.Name == name

może uzyskać dostęp do zmiennych w swoim zakresie, t oraz nazwie zmiennej, która jest w zakresie nadrzędnym. Mimo że jest wykonywany metodą find jako delegat, z innego zakresu razem.

DevelopisChris
źródło
2
Zamknięcie nie jest funkcją samą w sobie, jest bardziej zdefiniowane przez mówienie o zakresie niż funkcjach. Funkcje pomagają jedynie utrzymać zakres wokół, co powoduje utworzenie zamknięcia. Ale stwierdzenie, że zamknięcie jest funkcją, nie jest technicznie poprawne. Przepraszam za nitpick. :)
Jason Bunting