1. Giới thiệu
Làm việc với delegates và events trong C# rất dễ chịu và tiện — ngôn ngữ làm nhiều việc cho bạn. Tuy nhiên sau màn hình đó là nhiều cạm bẫy: leak bộ nhớ vô hình, bug lạ do đăng ký đôi, handlers lệch nhịp và thậm chí exception bất ngờ trong lúc gửi thông báo. Delegates và events mạnh mẽ nhưng cần làm chủ lifecycle của object, hiểu threading và biết chính xác cách gọi và hủy đăng ký hoạt động. Nếu events của bạn hoạt động "gần như luôn", nhưng thỉnh thoảng không chạy hoặc gây lỗi lạ — bạn không cô đơn đâu! Cùng xem chỗ nào thường vấp ngay cả với dev có kinh nghiệm và cách tránh.
Bảng các lỗi chính
| Lỗi | Hậu quả | Cách tránh |
|---|---|---|
| Đăng ký đôi | Handler bị gọi nhiều lần | Theo dõi chỗ đăng ký, hủy trước khi thêm |
| Không hủy đăng ký (memory leak) | Subscribers bị giữ trong bộ nhớ, “zombie” | Luôn hủy đăng ký, dùng IDisposable |
| Exception trong handler | Các handler còn lại sẽ không được gọi | Dùng try/catch trong handler hoặc duyệt thủ công |
| Sửa đổi đăng ký khi đang gửi event | Bị bỏ sót hoặc gọi trùng lặp | Duyệt bản sao danh sách handlers (GetInvocationList()) |
| Gọi event khi MyEvent == null | NullReferenceException | Kiểm tra null, dùng ?.Invoke |
| Signature handler không khớp | Lỗi biên dịch | Kiểm tra signature |
| Gọi event từ bên ngoài | Lỗi biên dịch | Chỉ gọi qua OnEventName |
| Sự kiện static không đúng chỗ | Đăng ký bị trộn lẫn | Đừng làm static nếu không cần |
| Vấn đề closure với lambda | Giá trị bất ngờ | Tạo bản sao biến |
2. Đăng ký nhiều lần và gọi nhiều lần
Bản chất lỗi
Nếu bạn đăng ký cùng một handler nhiều lần cho cùng một event, mỗi lần += sẽ thêm method của bạn vào hàng đợi delegate. Kết quả là handler sẽ được gọi bằng số lần nó được thêm vào.
Hiện tượng như thế nào?
Giả sử bạn có một nút và handler cho click:
Button btn = new Button();
btn.Click += OnButtonClick; // Đăng ký
btn.Click += OnButtonClick; // Đăng ký lại!
Giờ mỗi lần nhấn nút method OnButtonClick sẽ được gọi hai lần. Nếu trong handler bạn cập nhật counter hoặc thêm log, sẽ thấy kết quả bị nhân đôi.
Cách sửa?
Thường đăng ký lặp do cấu trúc code sai — ví dụ += nằm trong method có thể gọi nhiều lần (trong các lifecycle khác nhau của form).
- Theo dõi chỗ nào thực hiện đăng ký.
- Đừng đặt += ở các giai đoạn lifecycle có thể chạy nhiều lần.
- Thỉnh thoảng dùng "đăng ký duy nhất" — trước khi thêm thì hủy:
myEvent -= MyHandler; // Hủy cho chắc
myEvent += MyHandler; // Rồi đăng ký lại
Điều này an toàn: nếu handler chưa có thì -= không làm gì.
3. Subscriber zombie
Bản chất lỗi
Nếu subscriber đăng ký vào event của một publisher sống lâu hơn mà không hủy đăng ký, GC sẽ không thu dọn nó: publisher vẫn giữ tham chiếu tới delegate của handler, tức giữ cả object subscriber. Kết quả — leak bộ nhớ.
Ví dụ điển hình
public class TemporaryPopup : IDisposable
{
private Window _hostWindow;
public TemporaryPopup(Window window)
{
_hostWindow = window;
_hostWindow.Closed += OnHostClosed;
}
private void OnHostClosed(object sender, EventArgs e)
{
// ...
}
public void Dispose()
{
_hostWindow.Closed -= OnHostClosed; // Đừng quên hủy đăng ký!
}
}
Nếu quên gọi Dispose() hoặc không gọi nó, ngay cả khi bạn xóa mọi tham chiếu tới TemporaryPopup, object sẽ không bị GC — window vẫn tham chiếu tới handler.
Cách tránh?
- Implement IDisposable cho subscriber nếu lifecycle của nó ngắn hơn publisher.
- Dùng pattern using hoặc gọi rõ ràng Dispose():
using (var popup = new TemporaryPopup(mainWindow))
{
// ...
} // Ở đây Dispose sẽ tự động được gọi
Trong ứng dụng GUI — hủy đăng ký khi đóng window/form (ví dụ trong handler đóng hoặc trong Dispose() của form).
4. Xử lý exception trong handlers
Bản chất lỗi
Khi event gọi hàng chục handler, mà một handler ném exception, các handler còn lại sẽ không được thực thi.
Minh họa
public event EventHandler MyEvent;
public void Raise()
{
MyEvent?.Invoke(this, EventArgs.Empty);
}
Nếu một trong các method đăng ký vào MyEvent ném exception, các handler khác sẽ không được gọi — chuỗi bị đứt.
Làm sao xử lý?
- Trong handler event thì xử lý exception cục bộ (try/catch) hoặc cố ý rethrow nếu cần.
- Trong kịch bản phức tạp — duyệt danh sách subscribers thủ công và cô lập lỗi từng handler:
var handlers = MyEvent?.GetInvocationList();
foreach (var handler in handlers)
{
try
{
handler.DynamicInvoke(this, EventArgs.Empty);
}
catch (Exception ex)
{
// Logging, recovery
}
}
5. Thay đổi danh sách subscribers trong lúc gửi event
Bản chất lỗi
Nếu handler bên trong event hủy đăng ký chính nó hoặc người khác, thứ tự gọi có thể bị lệch: một số handler bị bỏ qua hoặc bị gọi lại.
Cách tránh?
- Đừng sửa đổi đăng ký từ bên trong handler.
- Nếu cần — duyệt bản sao của danh sách delegates:
var handlers = MyEvent?.GetInvocationList();
foreach (EventHandler handler in handlers)
{
handler(this, EventArgs.Empty);
}
6. Nhầm lẫn với delegate null (không có subscribers)
Bản chất lỗi
Nếu không ai đăng ký event thì delegate bằng null. Gọi mà không kiểm tra sẽ dẫn tới NullReferenceException.
Ví dụ (không nên)
public event EventHandler MyEvent;
public void Raise()
{
MyEvent(this, EventArgs.Empty); // Nếu không có subscriber — exception!
}
Làm đúng?
- Dùng gọi an toàn: MyEvent?.Invoke(this, EventArgs.Empty).
- Hoặc dùng cách cổ điển an toàn với luồng: copy delegate vào biến local rồi gọi biến đó.
7. Trộn lẫn delegates/events với signature khác nhau
Bản chất lỗi
Delegates có kiểu chặt. Không trùng signature giữa method handler và delegate event sẽ là lỗi biên dịch.
Ví dụ
public event EventHandler<string> TextChanged;
void WrongHandler(object sender, int number) { /* ... */ }
TextChanged += WrongHandler; // Lỗi biên dịch!
Dùng delegate chuẩn EventHandler và EventHandler<T>, và đảm bảo signature khớp đúng.
8. Cố gắng gọi event từ bên ngoài lớp publisher
Bản chất lỗi
event — là delegate được đóng gói: code bên ngoài không thể gọi trực tiếp (chỉ thêm/xóa handler được).
Ví dụ
public class MyPublisher
{
public event EventHandler SomethingHappened;
}
var publisher = new MyPublisher();
publisher.SomethingHappened?.Invoke(publisher, EventArgs.Empty); // Lỗi!
Cách đúng?
Gọi thông qua method được bảo vệ/không bảo vệ của publisher — thường là OnEventName. Bên ngoài chỉ dùng +=/-=.
9. Lỗi với accessors add và remove
Bản chất lỗi
Accessor tùy chỉnh cho events cho phép kiểm soát đăng ký, nhưng dễ làm sai thứ tự gọi hoặc làm mất thread-safety.
Ví dụ
public event EventHandler MyEvent
{
add { /* ... */ }
remove { /* ... */ }
}
Nếu không chắc — dùng implementation event chuẩn. Khi implement thủ công hãy đọc docs và nghĩ tới synchronization.
10. Truy cập sự kiện từ context static và non-static
Bản chất lỗi
Dễ vô tình khai báo event static trong khi nó nên là instance. Khi đó mọi object chia sẻ cùng hàng đợi subscribers.
Ví dụ
public static event EventHandler GlobalEvent; // Ui!
// Các instance mất tính riêng, đăng ký bị gộp chung
Cách tránh?
Chỉ làm event static khi cần mức global (ví dụ logging toàn cục). Trong hầu hết trường hợp — để event trên instance.
11. Vấn đề với capture biến trong lambda
Bản chất lỗi
Lambda capture biến theo tham chiếu. Trong vòng lặp điều này thường dẫn tới giá trị "cuối cùng".
Ví dụ
for (int i = 0; i < 5; i++)
{
button.Click += (s, e) => Console.WriteLine(i);
}
// Tất cả handler sẽ in "5"
Làm đúng?
for (int i = 0; i < 5; i++)
{
int copy = i; // Sao chép cục bộ
button.Click += (s, e) => Console.WriteLine(copy);
}
12. Trộn yếu và mạnh: Advanced “Weak Events”
Trong ứng dụng lớn (ví dụ WPF) dùng cơ chế "weak events", nơi publisher giữ weak reference tới subscriber để không ngăn GC. Weak events giúp tránh leak, nhưng subscriber có thể bị GC và không nhận event.
Chi tiết: Weak Event Patterns (MSDN)
13. Thiếu chuẩn đặt tên và signature cho events
Bản chất lỗi
Tuân theo signature và tên chuẩn: event đặt ở quá khứ (Changed, Closed, Completed), args kế thừa EventArgs.
Ví dụ "sai":
public delegate void SomethingHappens(int what);
// ...
public event SomethingHappens Something;
Ví dụ "đúng":
public event EventHandler<EventArgs> SomethingHappened;
Cho hầu hết event tự tạo hãy dùng EventHandler hoặc EventHandler<T>. Đồng nghiệp (và bạn trong tương lai) sẽ biết ơn.
GO TO FULL VERSION