C# – Tìm hiểu về Closure

dotNet_iconLiên quan đến delegate, closure là một khái niệm cần được hiểu rõ để tránh vấn đề khó hiểu mà bạn có thể gặp phải.

 

 

 

Để hiểu thế nào là closure trong C# bạn hãy xem ví dụ đơn giản sau và dự đoán kết quả được in ra:

var actions = new List<Action>();
for (int i = 0; i < 5; i++) { actions.Add(() => Console.WriteLine(i));
}

actions.ForEach(action => action());

Hãy so sánh dự đoán của bạn với kết quả mà đoạn code trên in ra khi nó được chạy:

10
10
10
10
10

Một ví dụ khác đơn giản và rõ ràng hơn:

int number = 1;
Action action = () => { Console.WriteLine(number); };
action(); // Output: 1
number = 2;
action(); // Output: 2
number = 3;
action(); // Output: 3

Như vậy bạn có thể thấy các Action (hay delegate) sẽ sử dụng các biến chung với môi trường mà delegate đó được tạo. Các biến mà bạn đem vào sử dụng bên trong delegate sẽ không phải là một giá trị cố định, tại thời điểm delegate được tạo ra.

Trở lại vòng lặp ví dụ sử dụng vòng lặp phía trên. Lý do các action đều in ra kết quả 10 là do biến i sau khi chạy hết vòng lặp thì sẽ có giá trị là 10. Sau đó danh sách action mới bắt đầu được thực thi và tất nhiên chúng đều cùng in ra giá trị của biến i tại thời điểm đó như bạn thấy.

Trong trường hợp bạn muốn cố định giá trị của delegate thì chỉ cần tạo ra một biến tạm để chứa giá trị và sử dụng nó trong delegate. Mỗi delegate sẽ sử dụng một biến riêng biệt và không bị thay đổi giá trị như sau:

var actions = new List<Action>();
for (int i = 0; i < 5; i++) { var j = i; actions.Add(() => Console.WriteLine(j));
}

actions.ForEach(action =&amp;amp;gt; action());

Output:

0
1
2
3
4

Hãy thử với vòng lặp foreach xem kết quả thế nào.

var numbers = new int[] { 0, 1, 2, 3, 4 };
var actions = new List<Action>);

foreach (var number in numbers)
{
actions.Add(() => Console.WriteLine(number));
}

actions.ForEach(action => action());

Output:

0
1
2
3
4

Bạn có thể khá ngạc nhiên vì kết quả này. Hai vòng lặp với kết quả khác nhau quả thật rất dễ gây nhầm lẫn. Sử dụng foreach lại khiến cho delegate được tạo ra có vẻ khiến chúng ta không thấy được sự hiện diện của closure. Nguyên nhân tất nhiên không phải .NET sai mà là do cơ chế của foreach. Thử dùng một trình reflector để xem lại chương trình, ta sẽ thấy lý do của nó.

int[] numArray = new int[] { 0, 1, 2, 3, 4 };
List<Action> list = new List<Action>();
int[] numArray2 = numArray;
for (int i = 0; i < numArray2.Length; i++) { int number = numArray2[i]; list.Add(() => Console.WriteLine(number));
}

Như bạn thấy foreach có cách hoạt động tương tự như vòng for thông thường và bên trong nó sử dụng một biến number để lưu giá trị của phần tử hiện tại trong mảng. Đó là nguyên nhân tại sao ví dụ phía trên lại có kết quả như thế.

Thay vì sử dụng một mảng int, nếu bạn sử dụng một List thì foreach sẽ thế nào?

var numbers = new int[] { 0, 1, 2, 3, 4 };

Thay bằng:

var numbers = new List { 0, 1, 2, 3, 4 };

Sử dụng reflector, bạn sẽ vẫn thấy nó tạo ra một biến có phạm vi bên trong vòng lặp (number).

List<int> list = new List<int> { 0, 1, 2, 3, 4 };
List<Action> list2 = new List<Action>();
using (List<int>.Enumerator enumerator = list.GetEnumerator())
{
while (enumerator.MoveNext())
{
int number = enumerator.Current;
list2.Add(() => Console.WriteLine(number));
}
}

Đây là bản chất của foreach nên chúng ta chấp nhận sự khác biệt với vòng lặp for. Phần chúng tả chỉ cần hiểu rõ để sử dụng hiệu quả và tránh được các vấn đề có thể nảy sinh.

YinYangIT Blog

5 bình luận về “C# – Tìm hiểu về Closure

  1. Anh, cho em hỏi, game rắn ăn mồi, là mình áp dụng thuật toán tìm kiếm mù là theo chiều rộng hay sâu anh, tại em có đọc là anh nói áp dụng theo chiều sâu mà chiều sâu là DFS chứ đâu phải là BFS , anh có thể trả lời kĩ giúp em em đang làm tiểu luận phần này ạ, nhưng demo bên c#, tks anh 🙂

Đã đóng bình luận.