1. Giới thiệu
Khi làm việc với chương trình, bạn không tránh khỏi những tình huống khi một phần phải "thông báo" cho phần khác rằng đã xảy ra điều gì đó quan trọng. Ví dụ cổ điển — người dùng click chuột, và sự kiện đó cần được xử lý. Trong cuộc sống hàng ngày chúng ta cũng sống giữa các sự kiện: ấm nước trên bếp kêu — bạn nghe tiếng và vội tắt bếp. Cà phê đổ lên bàn phím — tim giật một cái — và bạn chạy cứu laptop. Lập trình cũng tuân theo cùng quy tắc.
Sự kiện — là cơ chế cho phép đối tượng nguồn (nhà xuất bản) thông báo cho các đối tượng khác (người đăng ký) về những thay đổi hoặc hành động đã xảy ra. Nó giống như "tôi đã báo — ai chú ý thì phản hồi".
Trong C# events — là cấu trúc đặc biệt dựa trên kiểu delegate. Delegate xác định chữ ký callback — cái gì và cách gì sẽ được gọi ở phía người đăng ký. Khai báo event dùng từ khóa event, còn delegate — từ khóa delegate.
Tại sao cần events?
- Coupling lỏng hơn: Nhà xuất bản không biết gì về người đăng ký — chỉ phát tín hiệu.
- Kiến trúc linh hoạt: Có thể thêm bớt handlers động mà không thay code của nhà xuất bản.
- Mở rộng dễ dàng: Thêm người đăng ký mới — họ ngay lập tức nhận thông báo.
Mẫu cổ điển «Nhà xuất bản-Đăng ký»
Hãy tưởng tượng chúng ta có lớp "Báo cháy" (nhà xuất bản) và lớp "Người trong tòa nhà" (người đăng ký). Khi báo cháy kêu, nó phát tín hiệu cho tất cả mọi người cùng lúc — không quan trọng có bao nhiêu người ở trong tòa nhà và họ ở đâu. Đó chính là mẫu «Nhà xuất bản-Đăng ký» (hoặc Observer).
Nhà xuất bản không biết có bao nhiêu và những ai đăng ký — nó chỉ thông báo, những người khác tự đăng ký nhận thông báo hoặc bỏ qua.
Trong C# hoạt động thế nào?
- Nhà xuất bản: định nghĩa event (event), cung cấp đăng ký/hủy đăng ký.
- Người đăng ký: đăng ký vào event và triển khai handler (phương thức-handler sẽ được gọi khi event xảy ra).
2. Sự kiện trong thực tế: ví dụ đầu tiên
Từ lý thuyết sang code, mô tả một mô hình đơn giản. Giả sử ta có ứng dụng console, nơi một đối tượng timer mỗi giây "tích", và các handler khác nhau phản ứng (ví dụ in "tick" ra console hoặc đếm số tick).
Bước 1. Định nghĩa delegate và event
public class SimpleTimer
{
// Khai báo delegate cho sự kiện
public delegate void TickEventHandler(object sender, EventArgs e);
// Event, dựa trên delegate
public event TickEventHandler Tick;
public void Start(int count)
{
for (int i = 0; i < count; i++)
{
System.Threading.Thread.Sleep(1000); // giả lập tick!
OnTick(); // phát sự kiện!
}
}
protected virtual void OnTick()
{
// Gọi event nếu có người đăng ký (Tick != null)
Tick?.Invoke(this, EventArgs.Empty);
}
}
Chuyện gì đang xảy ra ở đây?
- Đã định nghĩa delegate TickEventHandler với chữ ký cổ điển object sender, EventArgs e.
- Sự kiện Tick — điểm để các handler đăng ký.
- Phương thức Start giả lập "tích tắc" và theo timer gọi OnTick.
- Trong OnTick sự kiện được gọi an toàn: Tick?.Invoke(..., EventArgs.Empty).
Bước 2. Đăng ký vào sự kiện
class Program
{
static void Main()
{
var timer = new SimpleTimer();
// Đăng ký vào sự kiện Tick
timer.Tick += Timer_Tick;
timer.Start(3);
// Có thể hủy đăng ký nếu cần
timer.Tick -= Timer_Tick;
}
static void Timer_Tick(object sender, EventArgs e)
{
Console.WriteLine("Tick!");
}
}
Chúng ta tạo timer, đăng ký bằng toán tử +=, rồi ở mỗi tick handler được gọi. Hủy đăng ký — toán tử -=.
3. Những điểm hữu ích cần lưu ý
Tại sao events tốt hơn gọi chặt chẽ?
Nếu SimpleTimer trong OnTick trực tiếp viết ra console, lớp sẽ bị liên kết cứng với hành động cụ thể đó. Events "giải phóng" code: timer không biết người đăng ký sẽ làm gì — phóng rocket, log vào file hay gửi e-mail.
Khác biệt quan trọng giữa event và delegate
- Delegate — "con trỏ" tới phương thức, còn event — là delegate với giới hạn truy cập.
- Người đăng ký chỉ có thể đăng ký/hủy; không thể gọi event từ bên ngoài — chỉ nhà xuất bản mới được gọi.
- Để khai báo event, thêm modifier event vào kiểu delegate — compiler đảm bảo mô hình truy cập đúng.
Sơ đồ ngắn về cách event hoạt động trong C#
+------------------+ +------------------------------+
| | | |
| Nhà xuất bản | <------> | Người đăng ký |
| (Publisher/Event)| | (Subscriber/Handler) |
| | | |
+------------------+ +------------------------------+
| 1) khai báo event | 2) đăng ký vào nó
| 3) gọi event | 4) triển khai handler
Khi nào nên dùng events?
- Cần thông báo tới số lượng listener không xác định về việc đã xảy ra điều gì.
- Không muốn liên kết logic hành động trong class nguồn.
- UI, tương tác bất đồng bộ, thông báo hệ thống — mọi thứ xoay quanh events.
Những đặc điểm ngắn gọn về triển khai events trong C#
- Event không thể được gọi "từ bên ngoài": chỉ mã của nhà xuất bản có quyền gọi Invoke.
- Đăng ký/hủy: toán tử +=/-=; có thể có nhiều handler.
- Event thực ra là danh sách các delegate: khi event xảy ra tất cả handler được gọi theo thứ tự đăng ký.
- Delegate được khuyến nghị: dùng EventHandler và EventHandler<TEventArgs> để tương thích với hệ sinh thái .NET.
4. Lỗi điển hình của người mới
Quên kiểm tra có người đăng ký: Tick != null. Tốt hơn là dùng gọi an toàn: Tick?.Invoke(...).
Đăng ký vào event nhưng không hủy khi handler không còn cần. Điều này có thể giữ các đối tượng trong bộ nhớ và gây memory leak.
Cố gắng "gọi" event từ lớp ngoài — compiler sẽ không cho phép. Không thể viết kiểu game.GameOver() nếu đó là event chứ không phải method.
Không tuân thủ chữ ký delegate. Với events hãy dùng chuẩn EventHandler hoặc EventHandler<TEventArgs> — như vậy code tương thích với thư viện .NET khác.
GO TO FULL VERSION