CodeGym /Các khóa học /C# SELF /Hủy đăng ký sự kiện ( -=

Hủy đăng ký sự kiện ( -=) và rò rỉ bộ nhớ

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

1. Giới thiệu

Theo một cách nào đó, đăng ký sự kiện trong C# giống như đăng ký nhận meme từ một người bạn: thông báo sẽ tới cho tới khi bạn nói "đủ rồi" và hủy đăng ký. Trong lập trình điều này đặc biệt quan trọng, vì quên hủy đăng ký — không phải chỉ là "thêm một meme nữa", mà là rò rỉ bộ nhớ!

Hãy tưởng tượng bạn có một form trong ứng dụng (ví dụ một cửa sổ cài đặt). Nó đăng ký vào sự kiện của cửa sổ chính để phản ứng với thay đổi. Người dùng đóng form, nghĩ là nó đã bị hủy, nhưng handler vẫn còn đăng ký! Form vẫn sống trong bộ nhớ vì publisher giữ tham chiếu đến nó qua sự kiện.

Kết luận: Nếu đối tượng-subscriber đã đăng ký vào sự kiện của publisher và "quên" hủy đăng ký, thì garbage collector sẽ không xóa nó khỏi bộ nhớ chừng nào publisher còn sống.

Ôn lại toán tử += và giới thiệu -=

  • += — đăng ký: thêm handler vào danh sách gọi của sự kiện.
  • -= — hủy đăng ký: xoá handler khỏi danh sách gọi.

Nó trông đại loại như thế này:


worker.WorkCompleted += handler; // đăng ký
worker.WorkCompleted -= handler; // hủy đăng ký

Nếu handler được thêm hai lần, bạn cần xoá nó cùng số lần để nó thực sự biến mất khỏi danh sách gọi.

Một chút nội bộ

Phía sau hậu trường, sự kiện trong C# là một field-delegate (hoặc danh sách delegate), và toán tử += thực chất gọi Delegate.Combine, còn -= thì gọi Delegate.Remove. Đối tượng đã đăng ký trở thành một phần của đồ thị tham chiếu. Đó là lý do vì sao quên đăng ký = rò rỉ bộ nhớ.

2. Rò rỉ bộ nhớ qua sự kiện: hoạt động như thế nào

Tình huống kinh điển


class Window
{
    public event EventHandler Updated;

    public void SimulateUpdate()
    {
        // Giả lập: gửi thông báo cho tất cả subscribers
        Updated?.Invoke(this, EventArgs.Empty);
    }
}

class SettingsForm
{
    public void OnWindowUpdated(object sender, EventArgs e)
    {
        Console.WriteLine("SettingsForm phản ứng với cập nhật cửa sổ");
    }
}

Hãy đi từng bước:


var window = new Window();
var settingsForm = new SettingsForm();

window.Updated += settingsForm.OnWindowUpdated;

window.SimulateUpdate(); // SettingsForm phản ứng

// Người dùng đóng form. Chúng ta mất mọi tham chiếu đến nó:
settingsForm = null;

// Nhưng đối tượng SettingsForm SẼ KHÔNG bị GC thu dọn miễn là window còn sống,
// vì window.Updated vẫn giữ tham chiếu tới phương thức OnWindowUpdated,
// và do đó giữ tham chiếu tới chính đối tượng SettingsForm.

Phải làm gì?
Hủy đăng ký:


// Để làm việc này cần ta còn giữ tham chiếu tới handler hoặc đối tượng:
window.Updated -= settingsForm.OnWindowUpdated;
settingsForm = null; // Bây giờ đối tượng có thể bị thu dọn

Bảng: ai giữ tham chiếu tới ai

Hành động Ai giữ tham chiếu Có thể giải phóng bộ nhớ không?
Đăng ký sự kiện (+=) Publisher tới subscriber Không, chừng nào publisher còn sống
Hủy đăng ký (-=) Không Có, sau khi loại hết các tham chiếu bên ngoài
Không đăng ký Không Có

3. Cách tổ chức hủy đăng ký đúng

Xoá handler một cách rõ ràng

Điều này có thể thực hiện, ví dụ, khi cửa sổ hoặc form đóng lại:


class SettingsForm
{
    private readonly Window _window;

    public SettingsForm(Window window)
    {
        _window = window;
        _window.Updated += OnWindowUpdated;
    }

    public void Close()
    {
        _window.Updated -= OnWindowUpdated; // hủy đăng ký!
        // ở đây code để đóng (ví dụ Dispose, GC.SuppressFinalize, v.v.)
    }

    public void OnWindowUpdated(object sender, EventArgs e)
    {
        // Xử lý sự kiện
    }
}

Nếu SettingsForm bị huỷ bằng nút "Close", quan trọng là đừng quên gọi method nơi thực hiện hủy đăng ký (ví dụ Close()).

Sử dụng interface IDisposable

Với các đối tượng phức tạp đăng ký sự kiện và quản lý vòng đời của chính nó, tiện lợi khi triển khai IDisposable. Trong method Dispose() thực hiện mọi hủy đăng ký cần thiết.


class SettingsForm : IDisposable
{
    private readonly Window _window;

    public SettingsForm(Window window)
    {
        _window = window;
        _window.Updated += OnWindowUpdated;
    }

    public void OnWindowUpdated(object sender, EventArgs e)
    {
        // ...
    }

    public void Dispose()
    {
        _window.Updated -= OnWindowUpdated;
        // Ở đây giải phóng các tài nguyên khác
    }
}

Bây giờ SettingsForm có thể dùng trong khối using, gọi rõ ràng Dispose() hoặc tự động hoá giải phóng tài nguyên (ví dụ thông qua GC.SuppressFinalize trong các kiểu có finalizer).

4. Tương tác với lambda: rủi ro và mẹo

Nếu bạn đăng ký sự kiện bằng lambda mà không lưu lambda vào biến, bạn sẽ không thể hủy đăng ký!


// Đăng ký — lambda ẩn danh
window.Updated += (s, e) => Console.WriteLine("Lambda được gọi!");

// Bây giờ làm sao để hủy đăng ký? — Không thể!
window.Updated -= (s, e) => Console.WriteLine("Lambda được gọi!"); // Đây là delegate khác!

Phải làm thế nào?
Lưu lambda vào biến delegate:


EventHandler handler = (s, e) => Console.WriteLine("Lambda được gọi!");
window.Updated += handler;

// ... giờ có thể hủy đăng ký!
window.Updated -= handler;

5. Những điểm cần lưu ý hữu ích

Đặc điểm vòng đời đối tượng và sự kiện

Một vấn đề thường gặp khác là tham chiếu chéo qua sự kiện giữa hai đối tượng "sống lâu". Ví dụ, một cửa sổ đăng ký sự kiện của cửa sổ khác, cả hai được dùng liên tục, không bị hủy — và bộ nhớ bị tiêu tốn ngày càng nhiều.

Khuyến nghị: Luôn nhớ ai đăng ký ai và khi nào cần hủy đăng ký. Nếu vòng đời của subscriber bằng với publisher — OK. Nếu subscriber có thể sống ngắn hơn publisher, hãy triển khai hủy đăng ký rõ ràng.

Quy tắc chung: "Đăng ký thì phải hủy!"

  • Với các publisher sống lâu (ví dụ global, singleton, cửa sổ chính) — luôn triển khai hủy đăng ký ở phía subscriber.
  • Với các đối tượng tạm thời (ví dụ thông báo một lần hoặc khi subscriber sống lâu hơn publisher) — có thể bớt lo lắng, nhưng vẫn chú ý ngữ cảnh.
  • Dùng các tiếp cận như WeakEvent (sự kiện yếu) hoặc các framework hỗ trợ nếu bạn không muốn quản lý hủy đăng ký thủ công.

6. Những lỗi thường gặp khi hủy đăng ký

Hủy sai: handler phải là cùng một instance

Rất quan trọng là khi hủy đăng ký bạn chỉ định chính xác handler đã dùng lúc đăng ký. Nếu không, hủy đăng ký sẽ không có tác dụng.

Lỗi:


window.Updated += settingsForm.OnWindowUpdated;
// ...
window.Updated -= new SettingsForm().OnWindowUpdated; // Không hoạt động! Đây là một instance khác và delegate khác!

Đúng:


window.Updated -= settingsForm.OnWindowUpdated;

Nếu khi đăng ký dùng lambda ẩn danh và không lưu delegate, bạn không thể hủy nó vì đó sẽ là một instance delegate khác:


// Đăng ký
window.Updated += (s, e) => Console.WriteLine("Lambda!");

// Cố gắng hủy — sẽ không hoạt động!
window.Updated -= (s, e) => Console.WriteLine("Lambda!");

"Quên" hủy đăng ký

Rất thường người ta quên hủy đăng ký, đặc biệt khi subscriber sống lâu hơn publisher hoặc khi developer không hiểu rõ cơ chế sự kiện. Hậu quả là các đối tượng-subscriber ở lại trong bộ nhớ lâu hơn cần thiết, dẫn tới rò rỉ bộ nhớ và vấn đề về performance.

2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Đăng ký và hủy đăng ký sự kiện
Đăng ký và hủy đăng ký sự kiện
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION