Apa itu 'penutupan' di .NET?


195

Apa itu penutupan ? Apakah kita memilikinya di .NET?

Jika ada di .NET, bisakah Anda memberikan cuplikan kode (lebih disukai dalam C #) yang menjelaskannya?

Jawaban:


258

Saya punya artikel tentang topik ini . (Ada banyak contoh.)

Pada dasarnya, penutupan adalah blok kode yang dapat dieksekusi di lain waktu, tetapi yang mempertahankan lingkungan di mana ia pertama kali dibuat - yaitu masih dapat menggunakan variabel lokal dll dari metode yang membuatnya, bahkan setelah itu Metode telah selesai dieksekusi.

Fitur umum penutupan diimplementasikan dalam C # dengan metode anonim dan ekspresi lambda.

Berikut ini contoh menggunakan metode anonim:

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

Keluaran:

counter=1
counter=2

Di sini kita dapat melihat bahwa tindakan yang dikembalikan oleh CreateAction masih memiliki akses ke variabel penghitung, dan memang dapat meningkatkannya, meskipun CreateAction sendiri telah selesai.


57
Terima kasih, Jon. BTW, apakah ada sesuatu yang tidak Anda ketahui di .NET? :) Kepada siapa Anda pergi ketika Anda memiliki pertanyaan?
Pengembang

44
Selalu ada lebih banyak untuk dipelajari :) Saya baru saja selesai membaca CLR melalui C # - sangat informatif. Selain itu, saya biasanya meminta Marc Gravell untuk pohon WCF / binding / ekspresi, dan Eric Lippert untuk hal-hal bahasa C #.
Jon Skeet

2
Saya perhatikan itu, tetapi saya masih berpikir pernyataan Anda tentang hal itu sebagai "blok kode yang dapat dieksekusi di lain waktu" sama sekali tidak benar - tidak ada hubungannya dengan eksekusi, lebih berkaitan dengan nilai-nilai variabel dan ruang lingkup daripada eksekusi , per se.
Jason Bunting

11
Saya akan mengatakan bahwa penutupan tidak berguna kecuali mereka dapat dieksekusi, dan "di lain waktu" menyoroti "keanehan" untuk dapat menangkap lingkungan (yang mungkin telah hilang oleh waktu eksekusi). Jika Anda hanya mengutip setengah kalimat maka itu adalah jawaban yang tidak lengkap, tentu saja.
Jon Skeet

4
@ SLC: Ya, countertersedia untuk ditambahkan - kompilator menghasilkan kelas yang berisi counterbidang, dan kode apa pun yang merujuk pada counterakhirnya melalui turunan dari kelas itu.
Jon Skeet

22

Jika Anda tertarik melihat bagaimana C # mengimplementasikan Penutupan, baca "Saya tahu jawabannya (42) blognya"

Compiler menghasilkan kelas di latar belakang untuk merangkum metode anoymous dan variabel j

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

untuk fungsi:

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

Mengubahnya menjadi:

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

Hai, Daniil - Jawaban Anda sangat berguna dan saya ingin melampaui jawaban Anda dan menindaklanjuti tetapi tautan rusak. Sayangnya, googlefu saya tidak cukup baik untuk menemukan ke mana ia pindah.
Knox

10

Penutupan adalah nilai fungsional yang menahan nilai variabel dari ruang lingkup aslinya. C # dapat menggunakannya dalam bentuk delegasi anonim.

Untuk contoh yang sangat sederhana, ambil kode C # ini:

    delegate int testDel();

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

    }

Pada akhirnya, bar akan diatur ke 4, dan delegasi myClosure dapat diedarkan untuk digunakan di tempat lain dalam program ini.

Penutup dapat digunakan untuk banyak hal berguna, seperti eksekusi yang tertunda atau untuk menyederhanakan antarmuka - LINQ terutama dibangun menggunakan penutupan. Cara yang paling langsung berguna bagi sebagian besar pengembang adalah menambahkan pengendali acara ke kontrol yang dibuat secara dinamis - Anda dapat menggunakan penutupan untuk menambah perilaku saat kontrol digunakan, daripada menyimpan data di tempat lain.


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

Penutupan adalah fungsi anonim yang dilewatkan di luar fungsi yang dibuatnya. Itu memelihara setiap variabel dari fungsi di mana ia dibuat yang digunakannya.


4

Berikut adalah contoh buat untuk C # yang saya buat dari kode serupa di 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; };
}

Jadi, berikut adalah beberapa kode yang menunjukkan cara menggunakan kode di atas ...

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

Harapan itu agak membantu.


1
Anda telah memberikan contoh, tetapi tidak menawarkan definisi umum. Saya mengumpulkan dari komentar Anda di sini bahwa mereka 'lebih banyak tentang ruang lingkup', tapi pasti ada lebih dari itu
ladenedge

2

Penutupan pada dasarnya adalah blok kode yang bisa Anda berikan sebagai argumen ke suatu fungsi. C # mendukung penutupan dalam bentuk delegasi anonim.

Berikut ini adalah contoh sederhana:
Metode List.Find dapat menerima dan mengeksekusi sepotong kode (penutupan) untuk menemukan item daftar.

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

Dengan menggunakan sintaks C # 3.0 kita dapat menulis ini sebagai:

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

1
Saya benci teknis, tetapi penutupan lebih terkait dengan ruang lingkup - penutupan dapat dibuat dalam beberapa cara yang berbeda, tetapi penutupan bukan berarti, itu adalah akhirnya.
Jason Bunting

2

Penutupan adalah ketika suatu fungsi didefinisikan di dalam fungsi lain (atau metode) dan itu menggunakan variabel dari metode induk . Ini penggunaan variabel yang terletak di suatu metode dan dibungkus dengan fungsi yang didefinisikan di dalamnya, disebut penutupan.

Mark Seemann memiliki beberapa contoh penutupan yang menarik di posting blognya di mana ia melakukan paralel antara pemrograman oop dan fungsional.

Dan untuk membuatnya lebih detail

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

Penutupan adalah potongan kode yang mereferensikan variabel di luar dirinya, (dari bawahnya di stack), yang dapat dipanggil atau dieksekusi nanti, (seperti ketika suatu peristiwa atau delegasi didefinisikan, dan dapat dipanggil pada waktu tertentu di masa mendatang. ) ... Karena variabel luar yang potongan referensi kode mungkin keluar dari ruang lingkup (dan jika tidak akan hilang), fakta bahwa itu direferensikan oleh potongan kode (disebut penutupan) memberitahu runtime untuk "menahan "variabel tersebut dalam ruang lingkup sampai tidak lagi diperlukan oleh penutupan kode ...


Seperti yang saya sebutkan pada penjelasan orang lain: Saya benci teknis, tetapi penutupan lebih berkaitan dengan ruang lingkup - penutupan dapat dibuat dalam beberapa cara yang berbeda, tetapi penutupan bukanlah cara, itu adalah akhirnya.
Jason Bunting

1
Penutupan relatif baru bagi saya, jadi sangat mungkin saya salah paham, tapi saya mendapatkan bagian cakupannya .. Jawaban saya terfokus pada cakupan. Jadi saya tidak tahu apa komentar Anda mencoba untuk mengoreksi ... Apa lagi yang relevan dengan lingkup tetapi beberapa kode? (fungsi, metode anonim, atau apa pun)
Charles Bretana

Bukan kunci untuk penutupan bahwa beberapa "chunk of runnable code" dapat mengakses nilai variabel atau dalam memori yang secara sintaksis "di luar" dari ruang lingkup itu, setelah variabel itu seharusnya biasanya "keluar dari ruang lingkup" atau dihancurkan ?
Charles Bretana

Dan @Jason, jangan khawatir tentang teknis, ide penutupan ini adalah sesuatu yang saya perlukan beberapa saat untuk membuat kepala saya terlilit, dalam diskusi panjang dengan rekan kerja, tentang penutupan javascript ... tapi dia orang gila dan saya tidak pernah cukup mendapat melalui abstraksi dalam penjelasan nya ...
Charles Bretana

0

Saya telah mencoba memahaminya juga, jauh di bawah ini adalah potongan kode untuk Kode yang sama dalam Javascript dan C # menunjukkan penutupan.

  1. Hitung Tidak Ada Kali, setiap peristiwa telah terjadi atau tidak ada kali setiap tombol diklik.

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. hitung Total tidak ada kali peristiwa klik telah terjadi atau hitung total tidak ada klik terlepas dari kontrol.

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

Tiba-tiba, jawaban yang sederhana dan lebih pengertian dari buku C # 7.0 singkatnya.

Pra-syarat Anda harus tahu : Ekspresi lambda dapat merujuk variabel lokal dan parameter metode yang didefinisikan (variabel luar).

    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
    }

Bagian nyata : Variabel luar yang dirujuk oleh ekspresi lambda disebut variabel yang ditangkap. Ekspresi lambda yang menangkap variabel disebut closure.

Poin terakhir yang perlu diperhatikan : Variabel yang diambil dievaluasi ketika delegasi benar-benar dipanggil, bukan ketika variabel ditangkap:

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

0

Jika Anda menulis metode anonim sebaris (C # 2) atau (lebih disukai) ekspresi Lambda (C # 3 +), metode aktual masih dibuat. Jika kode itu menggunakan variabel lokal luar-lingkup - Anda masih harus meneruskan variabel itu ke metode.

misalnya ambil klausa Linq Where ini (yang merupakan metode ekstensi sederhana yang melewati ekspresi 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;
});

jika Anda ingin menggunakan i dalam ekspresi lambda, Anda harus meneruskannya ke metode yang dibuat.

Jadi pertanyaan pertama yang muncul adalah: haruskah itu diteruskan dengan nilai atau referensi?

Lulus dengan referensi (saya kira) lebih disukai ketika Anda mendapatkan akses baca / tulis ke variabel itu (dan inilah yang dilakukan C #; Saya kira tim di Microsoft menimbang pro dan kontra dan melanjutkan dengan referensi; Menurut Jon Skeet's artikel , Java berjalan dengan nilai).

Tetapi kemudian muncul pertanyaan lain: Di mana saya harus mengalokasikan itu?

Haruskah itu benar-benar / secara alami dialokasikan pada tumpukan? Nah, jika Anda mengalokasikannya di stack dan meneruskannya dengan referensi, mungkin ada situasi di mana ia hidup lebih lama dari frame stack itu sendiri. Ambil contoh ini:

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

Ekspresi lambda (dalam klausa Dimana) sekali lagi menciptakan metode yang mengacu pada i. Jika saya dialokasikan pada tumpukan Outlive, maka pada saat Anda menyebutkan whereItems, saya yang digunakan dalam metode yang dihasilkan akan menunjuk ke saya dari Outlive, yaitu ke tempat di tumpukan yang tidak lagi dapat diakses.

Ok, jadi kita membutuhkannya di heap.

Jadi apa yang dilakukan oleh kompiler C # untuk mendukung inline anonim / lambda ini, adalah menggunakan apa yang disebut " Penutup ": Ini menciptakan kelas pada Heap yang disebut ( agak buruk ) DisplayClass yang memiliki bidang yang berisi i, dan Fungsi yang benar-benar menggunakan Itu.

Sesuatu yang setara dengan ini (Anda dapat melihat IL yang dihasilkan menggunakan ILSpy atau ILDASM):

class <>c_DisplayClass1
{
    public int i;

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

Instantiate kelas itu dalam lingkup lokal Anda, dan menggantikan kode apa pun yang berkaitan dengan saya atau ekspresi lambda dengan instance penutupan itu. Jadi - kapan pun Anda menggunakan i dalam kode "ruang lingkup lokal" tempat saya didefinisikan, Anda sebenarnya menggunakan bidang instance DisplayClass.

Jadi jika saya akan mengubah "lokal" i dalam metode utama, itu sebenarnya akan mengubah _DisplayClass.i;

yaitu

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

itu akan mencetak 12, karena "i = 10" pergi ke bidang dispalyclass dan mengubahnya tepat sebelum enumerasi ke-2.

Sumber yang baik tentang topik ini adalah modul Bart De Smet Pluralsight ini (memerlukan pendaftaran) (juga mengabaikan penggunaan istilah "Hoisting" yang salah - maksudnya adalah variabel lokal (yaitu i) diubah untuk merujuk ke bidang DisplayClass baru).


Dalam berita lain, tampaknya ada beberapa kesalahpahaman bahwa "Penutupan" terkait dengan loop - seperti yang saya mengerti "Penutupan" BUKAN sebuah konsep yang terkait dengan loop , tetapi lebih kepada metode anonim / ekspresi lambda penggunaan variabel cakupan lokal - meskipun beberapa trik pertanyaan menggunakan loop untuk menunjukkannya.


-1

Penutupan adalah fungsi, yang didefinisikan dalam suatu fungsi, yang dapat mengakses variabel lokal dan juga induknya.

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

jadi fungsi di dalam metode find.

 t => t.Name == name

dapat mengakses variabel di dalam ruang lingkupnya, t, dan nama variabel yang ada dalam ruang lingkup orang tuanya. Meskipun dieksekusi oleh metode find sebagai delegasi, dari lingkup lain semuanya.


2
Penutupan bukan fungsi, karena itu lebih ditentukan dengan berbicara tentang ruang lingkup daripada fungsi. Fungsi hanya membantu menjaga ruang lingkup sekitar, yang menyebabkan penutupan dibuat. Tetapi untuk mengatakan penutupan adalah fungsi secara teknis tidak benar. Maaf untuk nitpick. :)
Jason Bunting
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.