1. Giới thiệu
Trong lập trình closure — không phải là cách đóng cửa trong JavaScript, mà là cơ chế mà một biểu thức lambda hoặc phương thức ẩn danh capture các biến từ ngữ cảnh bao quanh và "nhớ" chúng ngay cả sau khi block nơi chúng được khai báo đã kết thúc. Nói đơn giản, closure là một hàm nhớ điều kiện nơi nó được sinh ra, và giữ những giá trị đó như một chiếc vali nhỏ chứa đồ cá nhân (các biến).
Closure là một hàm cùng với môi trường (scope) tồn tại tại thời điểm nó được tạo.
Ví dụ đơn giản nhất về closure
Hãy xem thực tế:
Func<int> MakeCounter()
{
int count = 0;
return () =>
{
count++;
return count;
};
}
Gọi như sau:
var counter = MakeCounter();
Console.WriteLine(counter()); // 1
Console.WriteLine(counter()); // 2
Console.WriteLine(counter()); // 3
Hoạt động như thế nào?
- Biến count được khai báo trong method MakeCounter.
- Lambda () => { ... } được trả về bên ngoài và sống bây giờ ngoài method.
- Nhưng! Nó nhớ biến count, mặc dù method MakeCounter đã kết thúc từ lâu.
Đó chính là closure: lambda "bắt" (capture) biến count từ ngữ cảnh bao quanh.
Lambda capture chính xác cái gì?
- các biến local từ method bao quanh (scope),
- tham số của method,
- các biến trong block (for, foreach, etc.).
Quan trọng: biến được capture không theo giá trị mà theo reference! Nếu ta thay đổi biến trong closure, nó sẽ thay đổi "bên ngoài" nữa. Thực tế compiler của C# tạo một lớp trợ giúp cho những biến này — nhưng khái niệm này đủ để bạn dùng closure một cách tự tin.
2. Closure và lexical scope
Hoàn thiện ví dụ "factory functions" với closure:
Func<int, int> PowerFactory(int power)
{
return x =>
{
int result = 1;
for (int i = 0; i < power; i++)
result *= x;
return result;
};
}
Sử dụng:
var square = PowerFactory(2); // x^2
var cube = PowerFactory(3); // x^3
Console.WriteLine(square(5)); // 25
Console.WriteLine(cube(2)); // 8
Các function square và cube được tạo với các giá trị khác nhau của biến power, và mỗi cái nhớ giá trị riêng. Mỗi lần gọi PowerFactory tạo một "ba lô" riêng của các giá trị được capture.
3. Thay đổi các biến bị capture (mutation)
Đôi khi bạn tự hỏi: nếu trong vòng lặp tạo nhiều lambda capture biến vòng lặp thì sao? Ở đây dễ vấp lỗi.
Ví dụ: closure trong vòng lặp
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions)
action(); // ???
Bạn mong đợi gì? 0, 1, 2? Thực tế kết quả sẽ là:
3
3
3
Tại sao? Tất cả lambda "tham chiếu" cùng một biến i. Khi vòng lặp kết thúc i đã bằng 3, và đó là giá trị mà tất cả Action thấy.
Phiên bản sửa
Để mỗi lambda capture "giá trị riêng", tạo một biến mới trong thân vòng lặp:
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
int copy = i;
actions.Add(() => Console.WriteLine(copy));
}
foreach (var action in actions)
action(); // 0 1 2
Bây giờ copy là biến mới ở mỗi lần lặp, và closure capture đúng biến đó.
4. Ứng dụng closure trong các bài toán thực tế
Xử lý dữ liệu và callbacks
Khi bạn làm việc bất đồng bộ hoặc hoãn thực thi (event handlers, filtering, scheduling) — closure cho phép "gói" logic cùng với tham số. Ví dụ:
void ProcessList(List<int> list, int threshold)
{
var filtered = list.Where(x => x > threshold);
foreach (var item in filtered)
Console.WriteLine(item);
}
Ở đây lambda trong Where capture biến threshold.
Tạo "factory" cho hàm
Truyền tham số — nhận về function với tham số đó "nhúng sẵn". Kỹ thuật này tiện cho cấu hình filter, comparator cho sort, phản ứng UI, v.v.
Quản lý state
Đôi khi cần giữ một ít state mà không muốn tạo class riêng:
Func<string, string> CreateGreeting()
{
string prefix = "Hello";
return name =>
{
return $"{prefix}, {name}!";
};
}
5. Những điểm hữu ích cần biết
Bên dưới nắp: closure hoạt động thế nào trong C#
Mọi thứ được lambda capture, compiler chuyển thành một lớp trợ giúp: các biến trở thành field, và lambda trở thành method. Do đó state của các biến "sống" giữa các lần gọi.
Mỗi "factory" tạo ra một object nhỏ. Điều này bình thường — .NET quản lý tốt những object này và chỉ allocate khi cần.
Lưu ý: đừng capture object "to" nếu không cần
Nếu capture một object lớn (ví dụ form UI), nó sẽ không được thu hồi garbage cho tới khi lambda còn sống. Nguyên nhân điển hình gây memory leak là subscribe sự kiện bằng lambda capture context "nặng" và không unsubscribe (+=/-=).
Closure và vòng đời collection — ví dụ với LINQ
Closure làm LINQ linh hoạt: filters nhớ tham số của chúng.
List<string> colors = new List<string> { "Red", "Green", "Blue", "Yellow" };
string startsWith = "B";
var filtered = colors.Where(c => c.StartsWith(startsWith));
foreach (var color in filtered)
Console.WriteLine(color); // "Blue"
Nếu sau đó thay đổi startsWith, kết quả cũng thay đổi:
startsWith = "R";
foreach (var color in filtered)
Console.WriteLine(color); // "Red"
Điều này xảy ra vì closure tham chiếu tới cùng biến startsWith, và method StartsWith mỗi lần kiểm tra giá trị hiện tại.
6. Lỗi thường gặp khi làm việc với closure
Lỗi #1: capture biến thay đổi trước khi dùng.
Tình huống kinh điển trong vòng lặp: lambda trong closure "nhìn" cùng một biến vòng lặp, và tới lúc gọi thì biến đã có giá trị khác. Kết quả là function hoạt động với dữ liệu không như mong muốn. Sửa bằng cách tạo biến riêng trong vòng lặp.
Lỗi #2: capture quá nhiều context.
Closure kéo theo cả object, thay vì chỉ field/giá trị cần thiết. Điều này tạo dependencies không cần thiết và làm code khó hiểu. Chỉ capture thứ cần thiết.
Lỗi #3: giữ resources nặng và memory leak.
Subscribe sự kiện bằng lambda mà lambda capture object "nặng", và không unsubscribe — object không được thu hồi. Theo dõi lifetime của subscription và dùng unsubscribe rõ ràng (-=).
Lỗi #4: mất tính dễ quản lý của code.
Dùng closure quá mức làm khó theo dõi nguồn dữ liệu và thay đổi của chúng, đặc biệt khi closure được khai báo xa chỗ gọi. Giữ logic gần nhau và đừng lạm dụng.
GO TO FULL VERSION