常見模式:利用控件 Tag 屬性攜帶業務資料物件 (如樹節點綁定領域模型)。預期:控件從視覺樹移除並 Dispose() 後,資料物件應被 GC 回收。實際:記憶體佔用居高不下,GC.Collect() 後仍殘留大量資料物件。
簡化復現程式碼:建立含大量資料的物件指派給 holderButton.Tag,點擊按鈕移除控件、Dispose()、設為 null,結果只回收一半記憶體。
使用 ANTS Memory Profiler 分析物件保留鏈,發現殘留物件的根源竟是 Control.cachedLayoutEventArgs 這個內部欄位!


Control 內部為了效能,會快取 LayoutEventArgs 實例 (內部類別),並在佈局事件中重用。這個快取欄位 持有對控件本身的強參考 (或透過閉包/事件參數間接持有)。當控件從父容器移除並 Dispose() 時,內部快取未被清理,導致控件實例存活,進而透過 Tag 屬性強參考業務資料物件,形成保留鏈:
GC Root → ... → Control.cachedLayoutEventArgs → Control 實例 → Tag → 業務資料物件 (大量記憶體)
在 Dispose() 前或移除控件前,手動設定 holderButton.Tag = null; 切斷參考鏈。程式碼修改後,記憶體完美回收。
disposing == true 時,將 Tag、DataContext、自訂欄位等業務資料參考設為 null。control.Disposed += (s,e) => control.Tag = null;。WeakReference<T>、或外部字典 ConditionalWeakTable<Control, T> 維護對應關係,控件本身不持有強參考。cachedLayoutEventArgs 是 .NET Framework 內部實作細節 (非公開 API),行為可能跨版本變更。這揭示了一個核心教訓:「正確使用」框架 API (移除、Dispose、設 null) 仍可能因框架內部隱性快取導致洩漏。 開發者需建立「誰持有參考,誰負責清理」的防衛意識,並在關鍵資源釋放點主動切斷所有可疑參考鏈。