'Đóng cửa' trong .NET là gì?


195

A là gì đóng cửa ? Chúng ta có chúng trong .NET không?

Nếu chúng tồn tại trong .NET, bạn có thể vui lòng cung cấp đoạn mã (tốt nhất là trong C #) để giải thích không?

Câu trả lời:


258

Tôi có một bài viết về chính chủ đề này . (Nó có rất nhiều ví dụ.)

Về bản chất, bao đóng là một khối mã có thể được thực thi sau đó, nhưng nó duy trì môi trường mà nó được tạo lần đầu tiên - tức là nó vẫn có thể sử dụng các biến cục bộ, v.v. của phương thức đã tạo ra nó, ngay cả sau đó Phương thức đã thực hiện xong.

Tính năng chung của các bao đóng được triển khai trong C # bằng các phương thức ẩn danh và các biểu thức lambda.

Đây là một ví dụ sử dụng một phương thức ẩn danh:

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

Đầu ra:

counter=1
counter=2

Ở đây chúng ta có thể thấy rằng hành động được tạo bởi CreatAction vẫn có quyền truy cập vào biến đếm và thực sự có thể tăng nó, mặc dù chính CreatAction đã kết thúc.


57
Cảm ơn Jon. BTW, có điều gì mà bạn không biết trong .NET không? :) Bạn đi đến ai khi bạn có thắc mắc?
Nhà phát triển

44
Luôn có nhiều thứ để tìm hiểu :) Tôi vừa đọc xong CLR qua C # - rất nhiều thông tin. Ngoài ra, tôi thường hỏi Marc Gravell về các cây WCF / ràng buộc / biểu thức và Eric Lippert cho các thứ ngôn ngữ C #.
Jon Skeet

2
Tôi nhận thấy điều đó, nhưng tôi vẫn nghĩ rằng tuyên bố của bạn về nó là "một khối mã có thể được thực thi sau này" đơn giản là không chính xác - nó không liên quan gì đến thực thi, liên quan nhiều hơn đến các giá trị biến và phạm vi hơn là thực thi , mỗi gia nhập.
Jason Bunting

11
Tôi muốn nói rằng việc đóng cửa không hữu ích trừ khi chúng có thể được thực thi và "vào lúc sau" làm nổi bật "sự kỳ quặc" của việc có thể nắm bắt được môi trường (có thể đã biến mất theo thời gian thực hiện). Nếu bạn chỉ trích dẫn một nửa câu thì đó là một câu trả lời không đầy đủ, tất nhiên.
Jon Skeet

4
@SLC: Có, countercó sẵn để được tăng lên - trình biên dịch tạo ra một lớp có chứa một countertrường và bất kỳ mã nào đề cập đến countercuối cùng đều đi qua một thể hiện của lớp đó.
Jon Skeet

22

Nếu bạn muốn xem cách C # thực hiện Đóng, hãy đọc "Tôi biết câu trả lời (blog 42) của nó"

Trình biên dịch tạo ra một lớp trong nền để đóng gói phương thức anoymous và biến j

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

cho chức năng:

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

Biến nó thành:

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

Xin chào, Daniil - Bạn trả lời rất hữu ích và tôi muốn vượt ra ngoài câu trả lời của bạn và theo dõi nhưng liên kết bị hỏng. Thật không may, googlefu của tôi không đủ tốt để tìm nơi nó chuyển đến.
Knox

10

Đóng là các giá trị chức năng giữ các giá trị thay đổi từ phạm vi ban đầu của chúng. C # có thể sử dụng chúng dưới dạng đại biểu ẩn danh.

Đối với một ví dụ rất đơn giản, hãy lấy mã C # này:

    delegate int testDel();

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

    }

Khi kết thúc, thanh sẽ được đặt thành 4 và đại biểu myClenses có thể được chuyển qua để sử dụng ở nơi khác trong chương trình.

Đóng cửa có thể được sử dụng cho rất nhiều thứ hữu ích, như thực thi bị trì hoãn hoặc để đơn giản hóa các giao diện - LINQ chủ yếu được xây dựng bằng cách sử dụng các bao đóng. Cách ngay lập tức nhất có ích cho hầu hết các nhà phát triển là thêm trình xử lý sự kiện vào các điều khiển được tạo động - bạn có thể sử dụng các bao đóng để thêm hành vi khi điều khiển được khởi tạo, thay vì lưu trữ dữ liệu ở nơi khác.


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

Một bao đóng là một hàm ẩn danh được truyền bên ngoài hàm mà nó được tạo. Nó duy trì bất kỳ biến nào từ hàm mà nó được tạo mà nó sử dụng.


4

Dưới đây là một ví dụ có sẵn cho C # mà tôi đã tạo từ mã tương tự trong 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; };
}

Vì vậy, đây là một số mã cho biết cách sử dụng mã trên ...

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

Hy vọng rằng có phần hữu ích.


1
Bạn đã đưa ra một ví dụ, nhưng không đưa ra một định nghĩa chung. Tôi thu thập từ các ý kiến ​​của bạn ở đây rằng họ 'nhiều hơn về phạm vi', nhưng chắc chắn có nhiều điều hơn thế?
ladenedge

2

Về cơ bản đóng cửa là một khối mã mà bạn có thể chuyển làm đối số cho hàm. C # hỗ trợ đóng cửa dưới hình thức đại biểu ẩn danh.

Đây là một ví dụ đơn giản:
Phương thức List.Find có thể chấp nhận và thực thi đoạn mã (bao đóng) để tìm mục của danh sách.

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

Sử dụng cú pháp C # 3.0, chúng ta có thể viết như sau:

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

1
Tôi ghét phải là kỹ thuật, nhưng đóng cửa có liên quan nhiều hơn đến phạm vi - một đóng có thể được tạo theo một vài cách khác nhau, nhưng đóng cửa không phải là phương tiện, đó là kết thúc.
Jason Bunting

2

Một bao đóng là khi một hàm được định nghĩa bên trong một hàm (hoặc phương thức) khác và nó sử dụng các biến từ phương thức cha . Việc sử dụng các biến được định vị trong một phương thức và được bao bọc trong một hàm được định nghĩa bên trong nó, được gọi là một bao đóng.

Mark Seemann có một số ví dụ thú vị về việc đóng cửa trong bài đăng trên blog của mình , nơi anh ấy thực hiện một sự tương đồng giữa oop và lập trình chức năng.

Và để làm cho nó chi tiết hơn

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

Các bao đóng là các đoạn mã tham chiếu một biến bên ngoài, (từ bên dưới chúng trên ngăn xếp), có thể được gọi hoặc được thực hiện sau đó, (như khi một sự kiện hoặc đại biểu được xác định và có thể được gọi tại một thời điểm tương lai không xác định ) ... Bởi vì biến bên ngoài mà đoạn mã tham chiếu mã có thể nằm ngoài phạm vi (và nếu không sẽ bị mất), thực tế là nó được tham chiếu bởi đoạn mã (được gọi là bao đóng) bảo cho bộ thực thi "giữ "Biến đó trong phạm vi cho đến khi nó không còn cần thiết bởi đoạn mã đóng ...


Như tôi đã chỉ ra về lời giải thích của người khác: Tôi ghét phải kỹ thuật, nhưng việc đóng cửa có liên quan nhiều hơn đến phạm vi - việc đóng cửa có thể được tạo ra theo một vài cách khác nhau, nhưng việc đóng cửa không phải là phương tiện, đó là kết thúc.
Jason Bunting

1
Đóng cửa là tương đối mới đối với tôi, vì vậy tôi hoàn toàn có thể hiểu sai, nhưng tôi có phần phạm vi .. Câu trả lời của tôi tập trung vào phạm vi. Vì vậy, tôi đang thiếu những gì mà bình luận của bạn đang cố gắng sửa ... Điều gì khác có thể phạm vi có liên quan đến nhưng một số đoạn mã? (chức năng, phương thức ẩn danh hoặc bất cứ điều gì)
Charles Bretana

Không phải là chìa khóa để đóng cửa mà một số "đoạn mã có thể chạy được" có thể truy cập vào một giá trị biến hoặc trong bộ nhớ mà nó tổng hợp "bên ngoài" phạm vi của nó, sau khi biến đó thường bị "vượt khỏi phạm vi" hoặc bị phá hủy ?
Charles Bretana

Và @Jason, không phải lo lắng về kỹ thuật, ý tưởng đóng cửa này là thứ tôi mất một lúc để xoay quanh, trong các cuộc thảo luận dài với một đồng nghiệp, về việc đóng javascript ... nhưng anh ấy là một Lisp nut và tôi không bao giờ hoàn toàn hiểu được những điều trừu tượng trong lời giải thích của mình ...
Charles Bretana

0

Tôi cũng đã cố gắng để hiểu nó, bên dưới là các đoạn mã cho cùng một Mã trong Javascript và C # hiển thị đóng.

  1. Đếm số lần mỗi sự kiện đã xảy ra hoặc không có lần nào mỗi nút được nhấp.

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. đếm Tổng số lần không có sự kiện nhấp chuột đã xảy ra hoặc đếm tổng số lần nhấp bất kể kiểm soát.

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

Chỉ cần ra khỏi màu xanh, một câu trả lời đơn giản và dễ hiểu hơn từ cuốn sách tóm tắt C # 7.0.

Điều kiện tiên quyết bạn nên biết : Biểu thức lambda có thể tham chiếu các biến và tham số cục bộ của phương thức được xác định (biến ngoài).

    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
    }

Phần thực : Các biến ngoài được tham chiếu bởi biểu thức lambda được gọi là các biến được bắt. Một biểu thức lambda nắm bắt các biến được gọi là bao đóng.

Điểm cuối cùng cần lưu ý : Các biến được chụp được đánh giá khi đại biểu thực sự được gọi, không phải khi các biến được bắt:

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

0

Nếu bạn viết một phương thức ẩn danh nội tuyến (C # 2) hoặc (tốt nhất là) một biểu thức Lambda (C # 3 +), một phương thức thực tế vẫn đang được tạo. Nếu mã đó đang sử dụng một biến cục bộ phạm vi bên ngoài - bạn vẫn cần truyền biến đó cho phương thức nào đó.

ví dụ: lấy mệnh đề Linq Where này (là một phương thức mở rộng đơn giản vượt qua biểu thức 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;
});

nếu bạn muốn sử dụng i trong biểu thức lambda đó, bạn phải truyền nó cho phương thức đã tạo.

Vì vậy, câu hỏi đầu tiên đặt ra là: nó nên được thông qua bởi giá trị hoặc tham chiếu?

Chuyển qua tham chiếu là (tôi đoán) thích hợp hơn khi bạn có quyền truy cập đọc / ghi vào biến đó (và đây là những gì C # làm; Tôi đoán nhóm trong Microsoft đã cân nhắc những ưu và nhược điểm và đi theo tham chiếu; Theo Jon Skeet bài viết , Java đã đi với giá trị phụ).

Nhưng sau đó, một câu hỏi khác được đặt ra: phân bổ i ở đâu?

Nó nên thực sự / tự nhiên được phân bổ trên ngăn xếp? Chà, nếu bạn phân bổ nó trên ngăn xếp và chuyển nó theo tham chiếu, có thể xảy ra trường hợp nó tồn tại lâu hơn khung stack của chính nó. Lấy ví dụ này:

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

Biểu thức lambda (trong mệnh đề Where) một lần nữa tạo ra một phương thức tham chiếu đến một i. Nếu tôi được phân bổ trên stack Outlive, thì vào thời điểm bạn liệt kê whereItems, i được sử dụng trong phương thức được tạo sẽ trỏ đến i của Outlive, tức là đến một vị trí trong stack không còn truy cập được.

Ok, vậy chúng ta cần nó trên đống rồi.

Vì vậy, trình biên dịch C # làm gì để hỗ trợ ẩn danh / lambda nội tuyến này, là sử dụng cái được gọi là " Closures ": Nó tạo ra một lớp trên Heap được gọi là ( khá kém ) DisplayClass có trường chứa i và Hàm thực sự sử dụng nó

Một cái gì đó tương đương với điều này (bạn có thể thấy IL được tạo bằng ILSpy hoặc ILDASM):

class <>c_DisplayClass1
{
    public int i;

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

Nó khởi tạo lớp đó trong phạm vi cục bộ của bạn và thay thế bất kỳ mã nào liên quan đến i hoặc biểu thức lambda bằng thể hiện đóng đó. Vì vậy - bất cứ khi nào bạn đang sử dụng i trong mã "phạm vi cục bộ" nơi tôi đã được xác định, bạn thực sự đang sử dụng trường đối tượng DisplayClass đó.

Vì vậy, nếu tôi thay đổi "cục bộ" i trong phương thức chính, nó thực sự sẽ thay đổi _DisplayClass.i;

I E

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

nó sẽ in ra 12, vì "i = 10" đi đến trường phân tán đó và thay đổi nó ngay trước phép liệt kê thứ hai.

Một nguồn tốt về chủ đề này là mô-đun Pluralsight Bart De Smet (yêu cầu đăng ký) (cũng bỏ qua việc sử dụng sai thuật ngữ "Tời" - ý tôi (ý tôi) là biến cục bộ (tức là i) được thay đổi để tham chiếu đến trường DisplayClass mới).


Trong một tin tức khác, dường như có một số quan niệm sai lầm rằng "Đóng cửa" có liên quan đến các vòng lặp - vì tôi hiểu "Đóng cửa" KHÔNG phải là một khái niệm liên quan đến các vòng lặp , mà là các phương thức ẩn danh / biểu thức lambda sử dụng các biến trong phạm vi cục bộ - mặc dù một số mẹo câu hỏi sử dụng các vòng lặp để chứng minh nó.


-1

Một bao đóng là một hàm, được định nghĩa trong một hàm, có thể truy cập các biến cục bộ của nó cũng như cha mẹ của nó.

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

Vì vậy, hàm bên trong phương thức find.

 t => t.Name == name

có thể truy cập các biến trong phạm vi, t và tên biến trong phạm vi cha của nó. Mặc dù nó được thực thi bởi phương thức find với tư cách là một đại biểu, từ một phạm vi khác tất cả cùng nhau.


2
Một bao đóng không phải là một hàm, mỗi se, nó được định nghĩa nhiều hơn bằng cách nói về phạm vi hơn là các hàm. Các chức năng chỉ hỗ trợ trong việc giữ phạm vi xung quanh, khiến cho việc đóng cửa được tạo ra. Nhưng để nói rằng đóng cửa là một chức năng không đúng về mặt kỹ thuật. Xin lỗi cho nitpick. :)
Jason Bunting
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.