CodeGym /Các khóa học /C# SELF /Bắt giữ biến ( Closures

Bắt giữ biến ( Closures)

C# SELF
Mức độ, Bài học
Có sẵn

1. Closure là gì?

Trong lập trình, closure (closure) — là một hàm bắt giữ các biến từ ngữ cảnh bên ngoài. Nói đơn giản, nếu một biểu thức lambda hoặc method ẩn danh sử dụng biến được khai báo ở ngoài thân nó, thì hàm đó trở thành một closure. Nó "nhớ" giá trị của những biến đó tại thời điểm tạo.

Tương tự đời thường:
Hãy tưởng tượng bạn ghi một công thức bí mật lên một tờ giấy và bỏ vào phong bì. Dù tờ giấy sau này bị mất hoặc không thể tìm thấy trực tiếp (biến không còn truy cập trực tiếp), ai đó cầm phong bì (lambda) vẫn có quyền truy cập vào công thức.

Ví dụ đơn giản:

int x = 42;
Func<int> getX = () => x;
Console.WriteLine(getX()); // 42

Ở đây getX — là một closure, vì nó dùng biến x được khai báo bên ngoài.

2. Tại sao bắt giữ biến quan trọng?

Trong C# closure được dùng ở khắp nơi:

  • Trong collection và LINQ queries
  • Để truyền tham số cho event hoặc phương thức bất đồng bộ
  • Khi tạo event handlers bên trong vòng lặp
  • Để lưu "context" giữa các lần gọi khác nhau

Không có closure nhiều thực hành tiêu chuẩn trong C# sẽ trở nên không thể hoặc rất bất tiện.

Ví dụ thực tế

Giả sử ta làm ứng dụng nhắc việc: người dùng đặt một loạt nhắc nhở, và sau một lúc (một phút, một giờ, một tuần...) nó phải hiển thị thông điệp phù hợp. Dễ dàng truyền vào handler một lambda đã "nhớ" phải nhắc gì. Đó chính là bắt giữ biến — ví dụ điển hình.

3. C# thực hiện bắt giữ biến như thế nào

Đằng sau hậu trường C# làm mẹo nhỏ: khi có lambda dùng các biến bên ngoài, compiler tự động tạo một lớp hỗ trợ — display class. Tất cả các biến "bắt giữ" trở thành field của lớp này.

Minh họa sơ đồ như sau:


Biến bên ngoài ──► DisplayClass
                         ▲
                         │
                    Closure (lambda)

Minh họa bằng code

Đây là những gì xảy ra "dưới nắp":

int x = 5;
Func<int> f = () => x;
// Ở đây compiler làm khoảng như sau:
class DisplayClass
{
    public int x;
    public int Lambda() => x;
}
DisplayClass display = new DisplayClass();
display.x = 5;
Func<int> f = display.Lambda;

Điều này giải thích tại sao closure tiếp tục thấy giá trị hiện tại của biến, ngay cả sau khi thoát khỏi khối nơi biến được khai báo.

4. Giá trị biến bị "đóng băng" hay thay đổi?

Trong C# biến bị bắt giữ theo tham chiếu, không phải theo giá trị. Nghĩa là, nếu lambda dùng một biến và biến đó thay đổi ở nơi khác — lambda sẽ thấy giá trị mới.

Ví dụ:

int x = 10;
Func<int> getX = () => x;

x = 20;
Console.WriteLine(getX()); // 20, chứ không phải 10!

Sinh viên thường mong getX() luôn trả 10 vì nghĩ rằng biến đã "bắt giữ". Tuy nhiên thực tế lambda đọc biến vẫn tồn tại và có thể bị thay đổi.

Khi nào giá trị thực sự được cố định?

Nếu biến được khai báo trong vòng lặp với phạm vi mới ở mỗi lần lặp, ví dụ dùng foreach, và mỗi lần lặp tạo biến mới — lambda sẽ "nhớ" giá trị tại lần đó.

5. Ví dụ: bắt giữ trong vòng lặp — bẫy thường gặp

Lỗi thường gặp

Muốn tạo mảng delegate, mỗi delegate in ra số thứ tự của nó từ vòng lặp:

Action[] actions = new Action[5];
for (int i = 0; i < 5; i++)
{
    actions[i] = () => Console.WriteLine(i);
}
foreach (var action in actions)
    action();

Chương trình sẽ in gì?

5
5
5
5
5

Ồ! Tại sao không phải 0,1,2,3,4?

Lý do:
Lambda bắt giữ cùng một biến i, biến này tiếp tục thay đổi trong vòng lặp. Khi sau này gọi delegate, i đã là 5.

Làm thế nào cho đúng?

Cần tạo một biến riêng trong thân vòng lặp:

Action[] actions = new Action[5];
for (int i = 0; i < 5; i++)
{
    int index = i; // Biến mới cho mỗi lần lặp!
    actions[i] = () => Console.WriteLine(index);
}
foreach (var action in actions)
    action();

Bây giờ chương trình sẽ in:

0
1
2
3
4

Điều này liên quan gì đến display class?

Trong phiên bản đầu tiên tất cả delegate "chung" một field — nên kết quả giống nhau. Ở phiên bản thứ hai tạo biến cục bộ mới cho mỗi lần lặp, nên với mỗi delegate sinh ra một DisplayClass riêng với giá trị độc lập.

6. Kịch bản thực tế dùng bắt giữ biến

Ví dụ 1: Xử lý sự kiện với "context"

Giả sử trong ứng dụng nhỏ của ta có danh sách task, và mỗi task có handler cho nút "complete". Cần để lambda bên trong handler "nhớ" task cần xử lý:

foreach (var task in tasks)
{
    button.Click += (sender, e) => CompleteTask(task);
}

Ở đây biến task được bắt giữ ở mỗi lần lặp. Quan trọng đảm bảo nó được khai báo đúng trong vòng lặp để không rơi vào bẫy như ví dụ trên.

Ví dụ 2: Toán tử bất đồng bộ

Thường dùng closure để truyền tham số vào logic bất đồng bộ — ví dụ lưu biến vào "slot" cục bộ khi khởi chạy task bất đồng bộ:

for (int i = 0; i < 3; i++)
{
    int index = i; // Bắt buộc!
    Task.Run(() => Console.WriteLine($"Task #{index}"));
}

Nếu không có biến cục bộ thì tất cả task sẽ in cùng một số, điều thường không mong muốn.

Ví dụ 3: LINQ queries

LINQ lên collection thường dùng closure để lọc hoặc chuyển đổi phần tử dựa trên biến từ phạm vi bên ngoài. Ví dụ:

string prefix = "Task";
var filtered = tasks.Where(t => t.Name.StartsWith(prefix));

Ở đây lambda trong Where đã nhớ giá trị của prefix và gọi method StartsWith.

7. Đặc điểm, giới hạn và lỗi thường gặp khi làm việc với closure

Lỗi #1: dùng một biến trong vòng lặp cho tất cả delegate.
Nếu trong vòng lặp tất cả delegate tham chiếu đến cùng một biến, kết quả sẽ bất ngờ. Cần tạo biến cục bộ mới trong vòng lặp cho mỗi delegate để tránh tham chiếu chung.

Lỗi #2: closure bắt giữ biến ngoài phương thức.
Nếu closure bắt giữ field của lớp hoặc biến khai báo ngoài phương thức hiện tại, nó sẽ giữ tham chiếu tới biến đó. Điều này có thể dẫn đến leak bộ nhớ, vì garbage collector không thể giải phóng đối tượng miễn còn tham chiếu từ closure.

Lỗi #3: delegate sống lâu có closure.
Nếu delegate có closure được lưu trữ lâu (ví dụ trong field tĩnh), các biến mà nó tham chiếu cũng sẽ ở trong bộ nhớ lâu hơn mong đợi. Điều này thường gây leak bộ nhớ ẩn và vấn đề hiệu năng.

2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Closure đơn giản
Closure đơn giản
1
Khảo sát/đố vui
, cấp độ , bài học
Không có sẵn
Biểu thức lambda
Cú pháp biểu thức lambda
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION