1. Giới thiệu
Sự kiện trong C# — không chỉ là biến để gán delegate. Đó là một danh sách handler được bảo vệ, và chỉ chủ sở hữu của sự kiện mới có thể khởi chạy nó; những phần khác chỉ có thể thêm (+=) hoặc gỡ (-=) phản ứng của họ.
Ví dụ, bạn có thể đăng ký sự kiện như sau:
worker.WorkCompleted += Worker_WorkCompleted;
Nhìn qua thì giống phép cộng, nhưng thực tế khác. Ở bên dưới hood, sự kiện giữ một invocation list — tập các delegate cần gọi khi sự kiện xảy ra. Khi bạn viết +=, một handler mới được thêm vào danh sách đó.
Hãy tìm hiểu kỹ hơn xem bên trong xảy ra gì, chuỗi được hình thành như thế nào và những nuance có thể gặp khi đăng ký.
Delegate-chain là gì?
Nhắc lại, delegate trong C# là 'multicast': bạn có thể gán nhiều phương thức cho nó, và tất cả sẽ chạy theo thứ tự nếu delegate được gọi. Sự kiện tận dụng cơ chế này: giá trị của nó về cơ bản là một delegate với danh sách handler.
Trong thuật ngữ mã:
public event EventHandler<WorkCompletedEventArgs> WorkCompleted;
Khi ai đó đăng ký:
worker.WorkCompleted += MyHandler;
C# ở dưới hood làm xấp xỉ như sau:
- Lấy delegate hiện tại (danh sách handler).
- Gọi cho nó phương thức Delegate.Combine (ghép các handler).
- Ghi chuỗi đã cập nhật trở lại biến của sự kiện.
Sơ đồ:
| Thao tác | Danh sách handler nội bộ của sự kiện |
|---|---|
| Trước | null hoặc [Handler1] |
| Sau += Handler2 | [Handler1, Handler2] |
| Sau thêm nữa += H3 | [Handler1, Handler2, Handler3] |
Sự thật thú vị: cơ chế multicast-delegate không phải "ma thuật", mà là một triển khai rất cụ thể: delegate ở dưới hood chứa một mảng các phương thức cần gọi.
2. Cách hoạt động của đăng ký: giải thích dễ hiểu
Ta xét một ví dụ cụ thể. Giả sử có một publisher (Worker) và các subscriber (Logger, Notifier):
public class Worker
{
public event EventHandler<WorkCompletedEventArgs> WorkCompleted;
public void DoWork()
{
// ... công việc ...
OnWorkCompleted("Tác vụ hoàn tất!");
}
protected virtual void OnWorkCompleted(string message)
{
WorkCompleted?.Invoke(this, new WorkCompletedEventArgs { Message = message });
}
}
public class Logger
{
public void LogWorkCompleted(object? sender, WorkCompletedEventArgs e)
{
Console.WriteLine("Ghi: " + e.Message);
}
}
public class Notifier
{
public void ShowNotification(object? sender, WorkCompletedEventArgs e)
{
Console.WriteLine("Thông báo: " + e.Message);
}
}
Trong Main:
var worker = new Worker();
var logger = new Logger();
var notifier = new Notifier();
worker.WorkCompleted += logger.LogWorkCompleted;
worker.WorkCompleted += notifier.ShowNotification;
// Khởi chạy công việc
worker.DoWork();
Khi OnWorkCompleted được gọi, sự kiện sẽ gọi trước logger.LogWorkCompleted, rồi tới notifier.ShowNotification (theo thứ tự bạn đăng ký).
3. Những nuance hữu ích
Minh họa: sự kiện lưu handler như thế nào
+---------------------+
| Worker |
|---------------------|
| WorkCompleted Event | ---> [ LogWorkCompleted, ShowNotification ]
+---------------------+
Khi đăng ký, mỗi handler mới "móc" vào danh sách hiện có. Sau khi sự kiện được gọi, delegate sẽ gọi lần lượt tất cả các phương thức đã đăng ký.
Đăng ký nhiều lần cùng một phương thức
worker.WorkCompleted += logger.LogWorkCompleted;
worker.WorkCompleted += logger.LogWorkCompleted; // Hai lần!
Trong trường hợp này handler sẽ được gọi đúng số lần nó bị đăng ký — ở đây là hai lần liên tiếp.
Đăng ký bằng lambda
worker.WorkCompleted += (sender, e) => Console.WriteLine("Xử lý ẩn danh: " + e.Message);
Nếu đăng ký cùng một lambda nhiều lần — tương tự, nó sẽ được gọi nhiều lần mỗi khi sự kiện xảy ra. Tuy nhiên để ý: mỗi lambda tạo ra một đối tượng delegate riêng, nên không thể dễ dàng unsubscribe bằng cùng một biểu thức lambda (xem chi tiết trong bài 260 trở đi).
Cách hoạt động phía dưới: phân tích mức thấp
Sự kiện — là một property đặc biệt với hai accessor (add/remove) được gọi khi dùng += và -=. Nếu đơn giản hóa, compiler sinh xấp xỉ mã sau:
// Khoảng như thế này (đơn giản hóa)
public event EventHandler<WorkCompletedEventArgs> WorkCompleted
{
add { /* mã thêm handler */ }
remove { /* mã xóa handler */ }
}
Mặc định dùng implement chuẩn: delegate được combine bằng Delegate.Combine và remove bằng Delegate.Remove.
Điều này bảo vệ sự kiện: bên ngoài không thể gọi sự kiện trực tiếp (không thể worker.WorkCompleted(...);), chỉ có thể đăng ký hoặc hủy đăng ký.
Cơ chế đăng ký: vẽ sơ đồ
+----------------------+
| |
v v
+--------------------+ +----------------------+
| LogWorkCompleted | | ShowNotification |
+--------------------+ +----------------------+
^ ^
\______________________/
^
|
WorkCompleted Event
Đó là cái gọi là "Invocation List" — chuỗi các cuộc gọi.
Tại sao quan trọng: ý nghĩa thực tiễn
Hiểu cơ chế đăng ký — chìa khoá để quản lý kết nối giữa các object. Bạn có thể xây hệ thống phức tạp, nơi các thành phần dynamic đăng ký/hủy đăng ký sự kiện, mà không tạo ra phụ thuộc cứng. Đây là pattern tiêu chuẩn trong UI frameworks, game engines, server apps và thậm chí kiến trúc microservices hiện đại (ở đó ý tưởng tương tự nhưng ở mức hàng đợi).
Trong phỏng vấn thường hỏi cách model sự kiện C# hoạt động, vì sao sự kiện làm hệ thống linh hoạt và cách quản lý lifecycle của subscription đúng cách.
4. Những gì được (và không được) làm từ bên ngoài class
Được
- Đăng ký sự kiện (+=)
- Hủy đăng ký (-=)
Không được
- Gọi sự kiện trực tiếp
- Gán delegate cho sự kiện trực tiếp (worker.WorkCompleted = ... — lỗi!)
Những ràng buộc này được thực hiện nhờ từ khóa event. Nếu bạn khai báo delegate như một trường bình thường:
public EventHandler<WorkCompletedEventArgs> WorkCompleted; // không phải event!
— thì bất kỳ ai cũng có thể làm mọi thứ, kể cả nulling handlers, gây ra hỗn loạn và bugs tiềm ẩn. Đó là lý do hầu như luôn dùng chỉ event!
Có thể đăng ký cùng một phương thức cho nhiều sự kiện không?
Có! Gọi là "multisubscribe". Ví dụ:
worker.WorkCompleted += logger.LogWorkCompleted;
anotherWorker.WorkCompleted += logger.LogWorkCompleted;
Nếu cùng một phương thức được đăng ký trên nhiều object khác nhau, handler sẽ chạy cho cả hai sự kiện. Bên trong handler bạn có thể biết ai gọi sự kiện thông qua tham số sender.
Case thực tế: đăng ký động
Giả sử app có thể chạy nhiều task song song. Mỗi task tạo một object Worker, và cùng một handler được đăng ký — ví dụ method logger logger.LogWorkCompleted.
var allWorkers = new List<Worker>();
for (int i = 0; i < 10; i++)
{
var w = new Worker();
w.WorkCompleted += logger.LogWorkCompleted;
allWorkers.Add(w);
}
Kết quả: khi bất kỳ task nào hoàn thành, logger sẽ nhận thông báo và ghi lại chuyện đã xảy ra khi nào.
Thực tế: cách thấy ai đang đăng ký sự kiện
Trong code bình thường không thể biết trực tiếp có bao nhiêu handler đã đăng ký (sự kiện đóng gói delegate). Tuy nhiên trong class publisher bạn có thể truy cập delegate của sự kiện, ví dụ gọi GetInvocationList():
// Chỉ bên trong lớp phát sự kiện
var handlers = WorkCompleted?.GetInvocationList();
if (handlers != null)
Console.WriteLine($"Số người đăng ký: {handlers.Length}");
Điều này hữu ích nếu cần logic gửi tin hoặc debug không chuẩn (mặc dù tốt hơn chỉ dùng cho mục đích học tập!).
5. Lỗi thường gặp và đặc điểm cần chú ý
Handler không được thêm? Nó sẽ không chạy!
Nếu bạn không đăng ký thì handler sẽ không bao giờ được gọi. Trước khi gọi sự kiện luôn kiểm tra null (nếu không sẽ có NullReferenceException).
Đăng ký nhiều lần
Nếu bạn đăng ký cùng một phương thức nhiều lần cho một sự kiện — handler sẽ chạy tương ứng số lần. Đây thường là bẫy: một lần vô tình gọi += làm một thông báo biến thành nhiều thông báo giống nhau.
Phương thức static/instance
Bạn có thể đăng ký cả static lẫn instance methods. Quan trọng là chữ ký đúng.
public static void StaticHandler(object? sender, WorkCompletedEventArgs e) { /* ... */ }
worker.WorkCompleted += StaticHandler;
Lambda và đăng ký trong vòng lặp
Nếu trong vòng lặp bạn đăng ký lambda, hãy hiểu rõ biến mà lambda đang capture. Dễ vô tình capture giá trị không như mong đợi.
GO TO FULL VERSION