.NET'te 'kapaklar' nedir?


195

Bir nedir kapatma ? .NET'te var mı?

.NET'te mevcutlarsa, lütfen açıklayan bir kod snippet'i (tercihen C # olarak) sağlayabilir misiniz?

Yanıtlar:


258

Ben çok bu konuda bir makale . (Çok sayıda örneği var.)

Özünde, bir kapanış, daha sonraki bir zamanda yürütülebilen, ancak ilk oluşturulduğu ortamı koruyan bir kod bloğudur - yani, onu oluşturan yöntemin yerel değişkenlerini vb. yönteminin yürütülmesi tamamlandı.

Kapakların genel özelliği, C # 'da anonim yöntemler ve lambda ifadeleriyle uygulanır.

Anonim bir yöntem kullanan bir örnek:

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

Çıktı:

counter=1
counter=2

Burada, CreateAction tarafından döndürülen eylemin hala sayaç değişkenine erişimi olduğunu ve CreateAction'ın kendisi bitmiş olsa bile gerçekten artırabilir.


57
Teşekkürler Jon. BTW, .NET'te bilmediğiniz bir şey var mı? :) Sorularınız olduğunda kime gidersiniz?
Geliştirici

44
Öğrenmek için her zaman daha fazlası var :) CR üzerinden CLR okumayı yeni bitirdim - çok bilgilendirici. Bunun dışında genellikle Marc Gravell'den WCF / bağlayıcı / ifade ağaçları ve Eric Lippert'den C # dili şeyler isterim.
Jon Skeet

2
Bunu fark ettim, ama yine de "daha sonra çalıştırılabilecek bir kod bloğu" hakkındaki ifadenizin sadece yanlış olduğunu düşünüyorum - yürütme ile ilgisi yok, değişken değerlerle ve yürütme yerine kapsamla daha fazla ilgisi yok , kendi başına.
Jason Bunting

11
Kapanmaların çalıştırılamadığı sürece yararlı olmadığını söyleyebilirim ve "daha sonraki bir zamanda", çevreyi yakalayabilmenin "tuhaflığını" (aksi takdirde yürütme süresiyle ortadan kalkmış olabilir) vurgular. Eğer cümlenin sadece yarısını koyarsanız , o zaman bu eksik bir cevaptır.
Jon Skeet

4
@SLC: Evet, counterartırılmak için kullanılabilir - derleyici bir counteralan içeren bir sınıf oluşturur ve counterbu sınıfın bir örneğinden geçerek sonuçlanan herhangi bir kod oluşturur.
Jon Skeet

22

C # 'ın Closure'u nasıl uyguladığını görmekle ilgileniyorsanız "Cevabı biliyorum (42) blogu"

Derleyici, anoymous yöntemi ve j değişkenini kapsüllemek için arka planda bir sınıf oluşturur

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

fonksiyon için:

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

Şu hale getiriliyor:

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

Merhaba Daniil - Cevabınız çok faydalı ve cevabınızın ötesine geçmek ve takip etmek istedim ama bağlantı koptu. Maalesef, googlefu'm nereye taşındığını bulmak için yeterince iyi değil.
Knox

11

Kapaklar, orijinal kapsamlarından değişken değerlere dayanan fonksiyonel değerlerdir. C # onları anonim delegeler şeklinde kullanabilir.

Çok basit bir örnek için şu C # kodunu alın:

    delegate int testDel();

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

    }

Sonunda, çubuk 4'e ayarlanacak ve myClosure delegesi programın başka bir yerinde kullanılmak üzere etrafından geçirilebilir.

Kapaklar, gecikmeli yürütme veya arayüzleri basitleştirmek için birçok yararlı şey için kullanılabilir - LINQ esas olarak kapaklar kullanılarak oluşturulur. Çoğu geliştirici için kullanışlı olmanın en acil yolu, dinamik olarak oluşturulan denetimlere olay işleyicileri eklemektir - başka bir yerde veri depolamak yerine denetim başlatıldığında davranış eklemek için kapanışları kullanabilirsiniz.


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

Kapatma, oluşturulduğu işlevin dışına iletilen anonim bir işlevdir. Kullanıldığı işlevdeki tüm değişkenleri korur.


4

JavaScript'te benzer koddan oluşturduğum C # için yapmacık bir örnek:

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

Yani, yukarıdaki kodun nasıl kullanılacağını gösteren bazı kodlar ...

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

Umarım biraz yardımcı olur.


1
Bir örnek verdiniz, ancak genel bir tanım sunmadınız. Buradaki yorumlarınızdan 'kapsam hakkında daha fazla' olduklarını düşünüyorum, ama bundan daha fazlası var mı?
ladenedge

2

Kapanışlar, daha sonra çağrılabilen veya çalıştırılabilen (bir olay veya delege tanımlandığında olduğu gibi ve belirsiz bir süre sonra çağrılabilen), kendilerinin dışındaki bir değişkeni (altta yığının altından) referans alan kod parçalarıdır. ) ... Kod referanslarının yığınının dış değişkeni kapsam dışına çıkabileceği (ve aksi takdirde kaybolacağı) olduğundan, kod yığınına (kapanış denir) başvuruda bulunulması, çalışma zamanına "tutma" "kodun kapanış yığınına artık ihtiyaç duyulmadan bu kapsamdaki değişken ...


Başkasının açıklamasında belirttiğim gibi: Teknik olmaktan nefret ediyorum, ancak kapanışın kapsamla daha fazla ilgisi var - bir kapatma birkaç farklı yolla oluşturulabilir, ancak bir kapatma, araç anlamına gelmez, sondur.
Jason Bunting

1
Kapanışlar benim için nispeten yeni, bu yüzden yanlış anladım tamamen mümkündür, ancak kapsam bölümünü alıyorum. Cevabım kapsam üzerine odaklanıyor. Yani ben yr yorum düzeltmek için çalışıyor ne eksik ... Kapsam başka ne kod ile ilgili olabilir ama bazı kod yığını? (işlev, anonim yöntem veya her neyse)
Charles Bretana

Bazı "çalıştırılabilir kod yığınının" sözdiziminin kapsamının dışında "sözdizimsel olarak" dışına "değişken veya bellek içi değere erişebileceği bir kapatma anahtarı, bu değişken normalde" kapsam dışına çıkmış "veya imha edilmiş olmalıdır ?
Charles Bretana

Ve @Jason, teknik olmaktan endişe etmiyor, bu kapatma fikri kafamın etrafını sarmak için biraz zaman harcadığım bir şey, bir iş arkadaşıyla uzun süren tartışmalarda, javascript kapanışlarıyla ilgili ... ama o bir Lisp somunuydu ve ben asla açıklamalarındaki soyutlamaları
aştı

2

Temel olarak kapatma, bir işleve bağımsız değişken olarak iletebileceğiniz bir kod bloğudur. C #, anonim delegeler şeklinde kapanmaları destekler.

İşte basit bir örnek:
List.Find yöntemi, listenin öğesini bulmak için kod parçasını (kapatma) kabul edebilir ve yürütebilir.

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

C # 3.0 sözdizimini kullanarak bunu şöyle yazabiliriz:

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

1
Teknik olmaktan nefret ediyorum, ancak kapanışın kapsamla daha fazla ilgisi var - bir kapatma birkaç farklı yolla oluşturulabilir, ancak bir kapatma aracı değildir, amaç budur.
Jason Bunting

2

Kapatma, bir işlev başka bir işlevin (veya yöntemin) içinde tanımlandığında ve üst yöntemin değişkenlerini kullanmasıdır . Bir yöntemde bulunan ve içinde tanımlanan bir işleve sarılmış değişkenlerin bu kullanımına kapatma adı verilir .

Mark Seemann'ın blog yazısında oop ve fonksiyonel programlama arasında bir paralel olduğu bazı ilginç kapanış örnekleri var .

Ve daha detaylı hale getirmek için

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.

0

Ben de anlamaya çalışıyorum, iyi aşağıda Javascript ve C # kapatma gösteren aynı kod için kod parçacıkları vardır.

  1. Her olayın gerçekleştiği Sayıyı veya her bir düğmenin tıklanma sayısını sayın.

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. Kontrolden bağımsız olarak tıklama etkinliğinin gerçekleşme sayısı veya toplam tıklama sayısını sayma.

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

Sadece mavi dışında, C # 7.0 kitabından basit ve daha anlayışlı bir cevap.

Bilmeniz gereken ön koşul : Lambda ifadesi, tanımlandığı yöntemin yerel değişkenlerine ve parametrelerine başvurabilir (dış değişkenler).

    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
    }

Gerçek kısım : Bir lambda ifadesi tarafından referans verilen dış değişkenlere yakalanan değişkenler denir. Değişkenleri yakalayan lambda ifadesine kapatma adı verilir.

Dikkat edilecek son nokta : Yakalanan değişkenler, temsilci gerçekten çağrıldığında değil, değişkenler yakalandığında değerlendirilir:

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

0

Satır içi anonim bir yöntem (C # 2) veya (tercihen) bir Lambda ifadesi (C # 3 +) yazarsanız, gerçek bir yöntem hala oluşturulmaktadır. Bu kod bir dış kapsam yerel değişkeni kullanıyorsa - yine de bu değişkeni bir şekilde yönteme iletmeniz gerekir.

örneğin bu Linq Where yan tümcesini (lambda ifadesini geçen basit bir uzantı yöntemidir) alın:

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

Eğer bu lambda ifadesinde i kullanmak istiyorsanız, onu oluşturulan yönteme geçirmelisiniz.

Ortaya çıkan ilk soru şudur: değer veya referansla mı geçilmelidir?

Bu değişkene okuma / yazma erişimi kazandıkça referansla (sanırım) daha çok tercih edilir (ve bu C #'ın yaptığı şeydir; Microsoft'taki ekip artıları ve eksileri tartıp referansla gitti; Jon Skeet'e göre makale , Java by-value ile gitti).

Ama sonra başka bir soru ortaya çıkıyor: Bunu nereye tahsis ederim?

Aslında / doğal olarak yığına tahsis edilmeli mi? Peki, yığına tahsis edip referans olarak iletirseniz, kendi yığın çerçevesinden daha fazla olduğu durumlar olabilir. Bu örneği ele alalım:

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

Lambda ifadesi (Where yan tümcesinde) tekrar i'yi ifade eden bir yöntem oluşturur. İ Outlive yığınına ayrılırsa, whereItems öğesini numaralandırdığınızda, oluşturulan yöntemde kullanılan i Outlive i'yi, yani yığın içinde artık erişilemeyen bir yeri gösterir.

Tamam, öyleyse yığın üzerinde ihtiyacımız var.

Yani C # derleyicisinin bu satır içi anonim / lambda'yı desteklemek için yaptığı şey, " Kapanışlar " olarak adlandırılan şeyi kullanmaktır : Öbek üzerinde i içeren bir alanı olan (ve oldukça zayıf olan ) DisplayClass adlı bir sınıf oluşturur. o.

Buna eşdeğer bir şey (ILSpy veya ILDASM kullanılarak oluşturulan IL'yi görebilirsiniz):

class <>c_DisplayClass1
{
    public int i;

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

Bu sınıfı yerel kapsamınızda başlatır ve i veya lambda ifadesiyle ilgili tüm kodları bu kapatma örneğiyle değiştirir. Yani - i tanımlanmış "yerel kapsam" kodunda i her kullandığınızda, aslında bu DisplayClass örnek alanı kullanıyorsunuz.

Yani ana yöntemde "local" i değiştirirseniz, aslında _DisplayClass.i değişecektir;

yani

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

"i = 10" bu dispalyclass alanına gider ve 2. numaralandırmadan hemen önce değiştirir.

Konuyla ilgili iyi bir kaynak, bu Bart De Smet Pluralsight modülüdür (kayıt gerektirir) (ayrıca "Kaldırma" terimini hatalı kullanımını da göz ardı eder - ne demek istediğimi (yani) yerel değişkenin (yani i) referans olarak değiştirildiği anlamına gelir. yeni DisplayClass alanına).


Diğer yandan, "Kapanışlar" ın döngülerle ilgili olduğu konusunda bir yanılgı var gibi görünüyor - "Kapanışlar" ın döngülerle ilgili bir kavram değil , daha ziyade anonim yöntemler / lambda ifadelerinin yerel kapsamlandırılmış değişkenlerin kullanımı olduğunu anlıyorum - bazı hileler olsa da sorular bunu göstermek için döngüler kullanır.


-1

Kapatma, bir işlev içinde tanımlanan ve yerel değişkenlerine ve üst öğelerine erişebilen bir işlevdir.

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

böylece find yöntemi içindeki fonksiyon.

 t => t.Name == name

kapsamındaki değişkenlere, t ve ebeveyn kapsamındaki değişken adına erişebilir. Bulma yöntemi tarafından bir temsilci olarak yürütülmesine rağmen, başka bir kapsamdan hep birlikte.


2
Kapatma bir işlev değildir, aslında kapsamdan bahsederek işlevlerden daha çok tanımlanır. İşlevler yalnızca kapsamın korunmasına yardımcı olur ve bu da bir kapanışın oluşturulmasına neden olur. Ancak kapanmanın bir fonksiyon olduğunu söylemek teknik olarak doğru değildir. Nitpick için üzgünüm. :)
Jason Bunting
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.