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?
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:
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.
counter
có 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 counter
trường và bất kỳ mã nào đề cập đến counter
cuối cùng đều đi qua một thể hiện của lớp đó.
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);
}
}
Đó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.
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.
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.
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);
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.
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 ...
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.
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);
}
}
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);
}
}
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
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ó.
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.