首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# 事件註冊與註銷:避免記憶體洩漏與重複觸發的完整指南

     編輯:關於C語言

問題現場:關閉子視窗重開,事件觸發多次、拋 NullReferenceException

主視窗 frmMain 持有子視窗 frmChild 事件訂閱,子視窗訂閱主視窗事件。關閉子視窗時未註銷事件,導致:

  1. 主視窗事件叫用清單殘留已銷毀子視窗的委派 → 呼叫時拋異常。
  2. 重開子視窗,新實例再次訂閱 → 事件觸發多次(N 次開啟 = N 次觸發)。
  3. 委派目標方法在新實例上執行,但欄位為 null → NullReferenceException。

錯誤程式碼


// frmMain
frmChild.OnRequestUserList += ToDoOnRequestUserList; // 註冊

// frmChild.OnLoad
((IParentForm)this.MdiParent).OnUserListCreated += ToDoOnRequestUserReturned; // 註冊

// frmChild 關閉時:缺少註銷!

正確模式:IDisposable / FormClosed 註銷


// frmChild
protected override void OnFormClosed(FormClosedEventArgs e)
{
    // 關鍵:註銷主視窗事件
    if (this.MdiParent is IParentForm parent)
    {
        parent.OnUserListCreated -= ToDoOnRequestUserReturned;
    }
    base.OnFormClosed(e);
}

// 或實作 IDisposable
public void Dispose()
{
    if (this.MdiParent is IParentForm parent)
        parent.OnUserListCreated -= ToDoOnRequestUserReturned;
    // 其他資源釋放
}

核心原則

原則說明
誰註冊,誰註銷訂閱者負責在生命週期結束時 -= 移除處理常式
事件來源長壽命主視窗、單例、靜態事件來源存活久,易累積死委派 → 記憶體洩漏
弱事件模式長壽命來源建議用 WeakEventManager (WPF) 或自訂弱參考包裝
靜態事件極危險靜態事件永不釋放,訂閱者必須顯式註銷,否則永遠無法 GC

替代方案:避免雙向事件耦合

方案 A:公開方法直接呼叫


// frmMain 直接呼叫 frmChild 公開方法
frmChild.RefreshUserList(dataTable); // 簡單、明確、無事件開銷

方案 B:介面回呼


public interface IChildCallback
{
    void OnUserListReady(DataTable dt);
}

// frmChild 實作介面
public class FrmChild : Form, IChildCallback
{
    public void OnUserListReady(DataTable dt) { /* ... */ }
}

// frmMain 持有介面參考
private IChildCallback _child;
_child.OnUserListReady(dt); // 編譯期檢查、無事件註冊/註銷煩惱

方案 C:訊息總線 / 中介者模式


// 使用 MediatR、EventAggregator 等解耦發布/訂閱
_mediator.Publish(new UserListCreatedEvent(dt)); // 發布者不知訂閱者
// 訂閱者在 DI 容器註冊,生命週期由容器管理

進階:弱事件實作 (適用於長壽命來源)


public class WeakEvent where TEventArgs : EventArgs
{
    private readonly List>> _handlers = new();

    public void Subscribe(EventHandler handler)
    {
        _handlers.Add(new WeakReference>(handler));
    }

    public void Unsubscribe(EventHandler handler)
    {
        _handlers.RemoveAll(wr => !wr.TryGetTarget(out var h) || h == handler);
    }

    public void Raise(object sender, TEventArgs e)
    {
        _handlers.RemoveAll(wr => !wr.TryGetTarget(out _)); // 清理已死參考
        foreach (var wr in _handlers)
            if (wr.TryGetTarget(out var handler))
                handler(sender, e);
    }
}

偵錯技巧:檢視事件叫用清單


// 即時視窗或 Watch 視窗
frmMain.GetType().GetField("OnUserListCreated", BindingFlags.NonPublic | BindingFlags.Instance)
    .GetValue(frmMain) as MulticastDelegate
    ?.GetInvocationList()
    ?.Select(d => d.Target?.GetType().Name + "." + d.Method.Name)

結語

事件訂閱建立強參考,是 .NET 記憶體洩漏的頭號兇手。鐵律:訂閱者在 Dispose/FormClosed/Unload 時必須註銷。架構層面,優先考慮直接方法呼叫、介面回呼或訊息總線,降低雙向事件耦合。養成「註冊即思考註銷」習慣,寫出不漏記憶體、不重複觸發的穩健程式碼。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved