CodeGym /Các khóa học /C# SELF /Lỗi phổ biến với delegates và events

Lỗi phổ biến với delegates và events

C# SELF
Mức độ, Bài học
Có sẵn

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.

2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Xử lý ngoại lệ trong các handler sự kiện
Xử lý ngoại lệ trong các handler sự kiện
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION