1. Phong cách khai báo và đặt tên sự kiện
Sự kiện — không chỉ là delegate. Đó là một thực thể riêng để giao tiếp giữa các phần của ứng dụng, và khai báo của nó nên rõ ràng.
Sử dụng loại delegate đúng
Trong 99% trường hợp dùng các delegate chuẩn:
- EventHandler — cho sự kiện không có dữ liệu.
- EventHandler<TEventArgs> — khi cần truyền tham số.
Chuẩn hóa giúp bảo trì mã dễ hơn và tích hợp với thư viện .NET. Đừng tự chế delegate nếu EventHandler đã phù hợp.
public event EventHandler SomethingHappened; // Không có dữ liệu
public event EventHandler<MyEventArgs> DataReceived; // Có dữ liệu bổ sung
Nếu cần tuỳ chỉnh đặc biệt — khai báo delegate riêng, nhưng đó là trường hợp hiếm.
Đặt tên sự kiện
Trong .NET sự kiện thường được đặt ở thì quá khứ: Completed, Clicked, Changed, Received. Điều này nhấn mạnh rằng sự kiện đã xảy ra.
Ví dụ:
public event EventHandler DataLoaded; // Dữ liệu đã được tải
public event EventHandler<MessageEventArgs> MessageReceived; // Tin nhắn đã nhận
public event EventHandler Saving; // Quá trình lưu bắt đầu
Đôi khi dùng dạng Changing cho sự kiện "trước" khi thay đổi, để cho phép can thiệp.
2. Tổ chức lớp phát: phương thức ảo bảo vệ OnEvent
Luôn thêm phương thức protected virtual để gọi sự kiện: điểm gọi tập trung, dễ mở rộng khi kế thừa và hành vi có thể dự đoán.
public class FileLoader
{
public event EventHandler<FileLoadedEventArgs> FileLoaded;
protected virtual void OnFileLoaded(FileLoadedEventArgs e)
{
FileLoaded?.Invoke(this, e);
}
public void Load(string filename)
{
// ... logic tải file ...
OnFileLoaded(new FileLoadedEventArgs(filename));
}
}
public class FileLoadedEventArgs : EventArgs
{
public string FileName { get; }
public FileLoadedEventArgs(string fileName) => FileName = fileName;
}
Hãy để chỉ OnFileLoaded gọi sự kiện — như vậy dễ bảo trì và test hơn.
3. Quy tắc đăng ký và hủy đăng ký: vòng đời, IDisposable
Nếu thời gian sống của listener ngắn hơn publisher, phải hủy đăng ký trước khi listener bị hủy. Thường implement IDisposable và hủy trong Dispose().
public class TemporaryListener : IDisposable
{
private readonly Publisher _publisher;
public TemporaryListener(Publisher publisher)
{
_publisher = publisher;
_publisher.DataReceived += HandleData;
}
private void HandleData(object sender, EventArgs e)
{
// Xử lý dữ liệu
}
public void Dispose()
{
_publisher.DataReceived -= HandleData;
}
}
// Sử dụng với using:
using (var listener = new TemporaryListener(myPublisher))
{
// listener nghe sự kiện ở đây
}
// Sau khi ra khỏi using - Dispose được gọi, hủy đăng ký xảy ra
Nếu quên hủy đăng ký, publisher sẽ giữ tham chiếu đến delegate của listener — dẫn đến leak bộ nhớ và "đối tượng xác sống".
4. Gọi sự kiện an toàn theo luồng
Trong mã đa luồng, subscriber có thể được thêm/bỏ song song với việc gọi sự kiện. Điều này gây race và NullReferenceException. Dùng mẫu an toàn theo luồng: copy delegate vào biến cục bộ.
protected virtual void OnSomethingHappened()
{
EventHandler handler = SomethingHappened;
handler?.Invoke(this, EventArgs.Empty);
}
Với C# 6+ đơn giản hơn:
SomethingHappened?.Invoke(this, EventArgs.Empty);
5. Dùng EventArgs thay vì object
Đừng truyền dữ liệu qua object hay qua trường của lớp. Dùng kiểu chặt qua subclass của EventArgs.
public class DownloadCompletedEventArgs : EventArgs
{
public string FileName { get; }
public long Size { get; }
public DownloadCompletedEventArgs(string fileName, long size)
{
FileName = fileName;
Size = size;
}
}
public event EventHandler<DownloadCompletedEventArgs> DownloadCompleted;
6. Tài liệu hoá sự kiện và subscriber
Document: khi nào sự kiện được gọi, ý nghĩa các trường trong EventArgs, có cần hủy đăng ký và khi nào.
/// <summary>
/// Sự kiện phát ra sau khi dữ liệu được tải thành công.
/// </summary>
public event EventHandler<DataLoadedEventArgs> DataLoaded;
7. Gợi ý tổng quát về kiến trúc sự kiện
Tách biệt trách nhiệm
Publisher chỉ thông báo về sự kiện. Subscriber tự quyết định khi nào đăng ký và hủy đăng ký.
Tránh "spam" sự kiện
Đừng phát cùng một sự kiện hàng chục lần mỗi giây nếu không cần — gây tải thừa.
Không dùng sự kiện cho liên lạc hai chiều
Sự kiện phù hợp cho mô hình "một báo cáo — nhiều lắng nghe". Cho giao tiếp hai chiều xem xét interface, callback hoặc cơ chế khác.
Đừng giữ tham chiếu tới subscribers trong lớp
Đừng giữ tham chiếu rõ ràng tới subscribers — events và delegates sẽ làm việc đó tự động.
8. Antipatterns cổ điển
Sự kiện không kiểu hóa
public event Action<object> SomethingHappened; // Không rõ có gì bên trong
Xấu: phá vỡ kiểu, cần cast, mất khả năng bảo trì.
Quên hủy đăng ký
public class ShortLivedListener
{
public ShortLivedListener(Publisher p) =>
p.DataReceived += DoWork;
private void DoWork(object sender, EventArgs e) { /* ... */ }
// Không có Dispose, không hủy đăng ký => đối tượng xác sống!
}
Vi phạm SRP
Lớp vừa là publisher, vừa là subscriber, vừa là handler — trộn lẫn vai trò. Tách trách nhiệm ra.
9. Ứng dụng thực tế trong phỏng vấn và dự án
Trong nhiều dự án có mô hình publish-subscribe, tổ chức sự kiện tốt giúp mở rộng và bảo trì. Trong phỏng vấn thường yêu cầu:
- implement hệ thống sự kiện với kiểu hóa đúng,
- cho thấy quản lý vòng đời của subscribers,
- giải thích gọi sự kiện an toàn theo luồng.
Mã sự kiện sạch, có tài liệu và tổ chức đúng ngay lập tức giúp bạn nổi bật giữa các ứng viên.
GO TO FULL VERSION