1. Giới thiệu
Trong nhiều dự án C# hiện đại gần như không còn nhiều phương thức "bình thường" để xử lý sự kiện. Điều này do lambda cho phép khai báo handler ngay tại chỗ đăng ký (toán tử +=) một cách nhanh và ngắn gọn, nếu xử lý đơn giản và không tái sử dụng ở nơi khác. Điều này giống như bạn dán một mảnh note nhỏ lên máy pha cà phê với hướng dẫn "nhấn nút này" thay vì viết manual chi tiết và để trong thư mục riêng. Nếu nhiệm vụ cục bộ và dùng một lần, lambda là lựa chọn tuyệt vời!
Nơi sử dụng trong thực tế
- Trong ASP.NET (ví dụ, khi xử lý các sự kiện lifecycle của trang),
- Trong WPF/WinForms cho UI (ví dụ, khi click nút),
- Trong lập trình server (ví dụ, logic bên trong pipeline),
- Trong tests, khi handler không cần tên riêng.
Cú pháp: trông như thế nào trong code
Trước tiên xem cách xử lý sự kiện bằng phương thức thông thường:
// Khai báo sự kiện
public event EventHandler? MyEvent;
// Đăng ký sự kiện bằng phương thức bình thường
void Handler(object? sender, EventArgs e)
{
Console.WriteLine("Sự kiện đã xảy ra!");
}
public void Subscribe()
{
MyEvent += Handler;
}
Bây giờ — biến thể với biểu thức lambda (hàm ẩn danh):
public void Subscribe()
{
MyEvent += (sender, e) => Console.WriteLine("Sự kiện đã xảy ra (lambda)!");
}
Lưu ý: chúng ta không tạo phương thức riêng mà gán handler ngay khi đăng ký sự kiện. Chữ ký của lambda tự động khớp với kiểu sự kiện (EventHandler).
So sánh các cách tiếp cận
| Phương thức bình thường | Lambda | |
|---|---|---|
| Khối lượng code | Nhiều hơn (phương thức + đăng ký) | Ít hơn, mọi thứ tại chỗ |
| Tái sử dụng | Có thể tái sử dụng | Thường thì không |
| Tính cục bộ của code | Phân tán | Tất cả gần nhau |
| Rõ ràng/đọc được | Tốt cho logic phức tạp | Tuyệt cho các case đơn giản |
2. Ứng dụng với xử lý sự kiện và lambda
Cấu trúc cơ bản
public class Menu
{
public event EventHandler? ItemSelected;
public void SelectItem(int index)
{
Console.WriteLine($"Mục menu {index} được chọn.");
ItemSelected?.Invoke(this, EventArgs.Empty);
}
}
Đăng ký bằng biểu thức lambda
class Program
{
static void Main()
{
var menu = new Menu();
// Đăng ký sự kiện bằng lambda
menu.ItemSelected += (sender, e) =>
{
Console.WriteLine("Cảm ơn đã chọn! Lambda handler đã chạy.");
};
menu.SelectItem(1);
}
}
Output mong đợi:
Mục menu 1 được chọn.
Cảm ơn đã chọn! Lambda handler đã chạy.
Capture biến từ ngữ cảnh bên ngoài
Một lợi thế của lambda là nó có thể "ghi nhớ" giá trị từ scope bên ngoài (closure). Ví dụ, đếm số lần mục được chọn: biến counter sẽ bị capture bởi closure.
static void Main()
{
var menu = new Menu();
int counter = 0;
menu.ItemSelected += (s, e) =>
{
counter++;
Console.WriteLine($"Mục được chọn {counter} lần!");
};
menu.SelectItem(1);
menu.SelectItem(2);
}
Output mong đợi:
Mục menu 1 được chọn.
Mục được chọn 1 lần!
Mục menu 2 được chọn.
Mục được chọn 2 lần!
Đây là "phép màu" của closures: biến counter tiếp tục sống bên trong lambda!
Ví dụ với tham số sự kiện
Nếu sự kiện của bạn dùng EventHandler<T>, trong đó T là lớp tùy chỉnh với thông tin bổ sung, lambda chỉ cần "điều chỉnh" theo chữ ký cần thiết.
public class MenuItemSelectedEventArgs : EventArgs
{
public int ItemIndex { get; }
public string Description { get; }
public MenuItemSelectedEventArgs(int itemIndex, string description)
{
ItemIndex = itemIndex;
Description = description;
}
}
public class Menu
{
public event EventHandler<MenuItemSelectedEventArgs>? ItemSelected;
public void SelectItem(int index, string description)
{
Console.WriteLine($"Mục menu {index}: {description} được chọn.");
ItemSelected?.Invoke(this, new MenuItemSelectedEventArgs(index, description));
}
}
// Sử dụng
static void Main()
{
var menu = new Menu();
// Lambda unpack tham số của sự kiện
menu.ItemSelected += (sender, args) =>
{
Console.WriteLine($"Đã chọn mục #{args.ItemIndex}: {args.Description.ToUpper()}");
};
menu.SelectItem(3, "Về chương trình");
}
Output mong đợi:
Mục menu 3: Về chương trình được chọn.
Đã chọn mục #3: VỀ CHƯƠNG TRÌNH
3. Handlers cục bộ và lambda
Lambda lý tưởng khi:
- Logic xử lý ngắn và rõ ràng,
- Handler chỉ dùng ở một chỗ,
- Cần "capture" biến từ ngữ cảnh cục bộ.
Nếu xử lý phức tạp, cần tái sử dụng hoặc có thể gọi ngoài chỗ khai báo, tốt hơn nên dùng phương thức có tên riêng.
Ví dụ: logic nằm trong và ngoài
Lambda (lý tưởng):
button.Click += (s, e) => MessageBox.Show("Nút được nhấn!");
Phương thức (khi logic phức tạp hoặc cần tái sử dụng):
button.Click += Button_Click;
void Button_Click(object sender, EventArgs e)
{
if (UserConfirmed())
{
SaveData();
MessageBox.Show("Dữ liệu đã được lưu!");
}
}
4. Bên dưới lớp vỏ: chuyện gì xảy ra với lambda handlers
Lambda cũng là một delegate, giống như handler bình thường. Compiler tạo ra một method ẩn danh, và nếu có biến bị capture thì còn sinh một lớp ẩn để lưu trữ chúng.
Cần nhớ (và đây là lỗi phổ biến): nếu bạn tạo lambda trong vòng lặp và đăng ký nó cho sự kiện, tất cả các lần lặp có thể capture cùng một biến vòng lặp.
for (int i = 0; i < 5; i++)
{
buttons[i].Click += (sender, e) =>
{
Console.WriteLine($"Click nút #{i}");
};
}
Với cách trên, tất cả handler có thể in ra 5! Để tránh, tạo bản sao biến trong vòng lặp:
for (int i = 0; i < 5; i++)
{
int buttonIndex = i; // Bản sao cục bộ
buttons[i].Click += (sender, e) =>
{
Console.WriteLine($"Click nút #{buttonIndex}");
};
}
Bây giờ mọi thứ chạy như mong đợi.
Sức mạnh thực sự của lambda handlers: cuộc sống thực
Những lambda kiểu này tiết kiệm thời gian và tăng tốc phát triển, đặc biệt khi logic đơn giản. Chúng giúp code "gần với nhiệm vụ" hơn, không rải rác khắp file/class. Trong dự án thực tế bạn sẽ thấy chúng ở khắp nơi: từ xử lý sự kiện UI tới đăng ký message trên các event bus bất đồng bộ.
5. Lỗi điển hình
Lỗi #1: quên hủy đăng ký (-=) với các object sống lâu.
Nếu subscriber không hủy đăng ký từ publisher, tham chiếu delegate giữ subscriber trong memory — GC không thể thu gom nó, ngay cả khi không còn tham chiếu khác tới đối tượng. Kết quả là leak bộ nhớ và phụ thuộc treo, nhất là khi publisher sống lâu (sự kiện static, singletons, services).
Cách tránh: luôn hủy đăng ký khi giải phóng/huỷ (ví dụ, Dispose, OnDisable, OnDestroy). Xem xét weak references (weak events / WeakEventManager), pattern "event manager" hoặc IObservable/Rx nếu kịch bản phức tạp.
Lỗi #2: capture biến từ vòng lặp mà không tạo bản sao cục bộ.
Bẫy phổ biến là đăng ký trong vòng lặp và closure nắm cùng một biến vòng lặp, nên tất cả handler thấy giá trị cuối cùng của biến thay vì giá trị tại thời điểm tạo handler. Dẫn tới kết quả bất ngờ (tất cả handler in cùng một số, v.v.).
Cách tránh: trong vòng lặp tạo bản sao cục bộ và capture nó:
for (int i = 0; i < n; i++)
{
int current = i;
button.Click += (s, e) => Handle(current);
}
Hoặc truyền giá trị cần thiết vào method-wrapper. Cách này chắc chắn hơn và làm ý định rõ ràng.
Lỗi #3: dùng lambda nặng nề ngay tại chỗ đăng ký.
Nếu bạn đặt logic nghiệp vụ lớn trực tiếp trong hàm ẩn danh khi đăng ký, code trở nên khó đọc, khó test và debug. Thêm nữa — với lambda ẩn danh khó hủy đăng ký đúng cách vì cần cùng instance delegate. Kết quả là logic rải rác và mất cấu trúc.
Cách tránh: tách logic phức tạp ra method có tên hoặc service; khi cần lưu delegate thì lưu vào biến/field để có thể hủy đăng ký; để lambda chỉ làm wrapper/forward nhỏ. Điều này cải thiện readability và maintainability.
GO TO FULL VERSION