主視窗 frmMain 持有子視窗 frmChild 事件訂閱,子視窗訂閱主視窗事件。關閉子視窗時未註銷事件,導致:
NullReferenceException。
// frmMain
frmChild.OnRequestUserList += ToDoOnRequestUserList; // 註冊
// frmChild.OnLoad
((IParentForm)this.MdiParent).OnUserListCreated += ToDoOnRequestUserReturned; // 註冊
// frmChild 關閉時:缺少註銷!
// 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 |
// frmMain 直接呼叫 frmChild 公開方法
frmChild.RefreshUserList(dataTable); // 簡單、明確、無事件開銷
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); // 編譯期檢查、無事件註冊/註銷煩惱
// 使用 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 時必須註銷。架構層面,優先考慮直接方法呼叫、介面回呼或訊息總線,降低雙向事件耦合。養成「註冊即思考註銷」習慣,寫出不漏記憶體、不重複觸發的穩健程式碼。