在 .NET 開發中,正確釋放非受控資源(檔案控制代碼、資料庫連線、GDI+ 物件、COM 元件等)是撰寫健壯程式的關鍵。許多開發者對 IDisposable、Finalize (解構函式)、GC.SuppressFinalize 的運作機制存在誤解,導致資源洩漏或效能損耗。本文深入剖析兩種釋放機制,並提供標準 Dispose Pattern 實作範本。
| 特性 | Finalize (解構函式 ~ClassName) | Dispose (IDisposable) |
|---|---|---|
| 呼叫時機 | GC 非確定性時間,第 2 代回收時 | 開發者明確呼叫或 using 區塊結束 |
| 效能影響 | 物件進入 Finalization Queue,延遲 2 次 GC 才能回收 | 立即釋放,無額外 GC 負擔 |
| 可靠度 | 保底機制,防止開發者忘記呼叫 Dispose | 依賴開發者遵守約定 |
| 存取受控物件 | 不安全,受控物件可能已被回收 | 安全,可正常存取受控資源 |
當物件擁有解構函式時,GC 首次掃描會將其放入 Finalization Queue**,由專用執行緒逐一呼叫 Finalize。執行完畢後物件從佇列移除,但此時已晉升至第 1 代,必須等待下一次第 1 代或第 2 代 GC 才能真正回收記憶體。這意味著:
public class ResourceWrapper : IDisposable
{
// 非受控資源控制代碼 (IntPtr, Handle, COM 介面等)
private IntPtr _unmanagedHandle = IntPtr.Zero;
// 受控資源
private FileStream _managedStream;
// 狀態旗標
private bool _disposed = false;
public ResourceWrapper(string filePath)
{
_managedStream = new FileStream(filePath, FileMode.Open);
_unmanagedHandle = _managedStream.SafeFileHandle.DangerousGetHandle(); // 示範用
}
// ===== 核心釋放邏輯 =====
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
// 釋放受控資源
_managedStream?.Dispose();
_managedStream = null;
}
// 釋放非受控資源 (無論 disposing 真假皆需執行)
if (_unmanagedHandle != IntPtr.Zero)
{
CloseHandle(_unmanagedHandle); // P/Invoke
_unmanagedHandle = IntPtr.Zero;
}
_disposed = true;
}
// ===== 公開 Dispose =====
public void Dispose()
{
Dispose(true);
// 關鍵:告訴 GC 不需呼叫 Finalize,避免進入 Finalization Queue
GC.SuppressFinalize(this);
}
// ===== Finalize 保底機制 =====
~ResourceWrapper()
{
// 僅釋放非受控資源 (disposing = false)
Dispose(false);
}
// P/Invoke 範例
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
private static extern bool CloseHandle(IntPtr hObject);
}
true:由開發者主動呼叫,可安全存取受控物件,釋放「受控+非受控」資源false:由 Finalizer 呼叫,受控物件可能已失效,僅釋放非受控資源若 Dispose() 已完整釋放資源,呼叫此方法將物件從 Finalization Queue 移除,GC 下次掃描可直接回收記憶體,避免 Finalizer 額外開銷。
多次呼叫 Dispose() 或 Dispose() 後經過 Finalize 皆不會重複釋放,避免 ObjectDisposedException 或雙重釋放導致崩潰。
public class DerivedWrapper : ResourceWrapper
{
private bool _disposed = false;
private AnotherResource _extra;
protected override void Dispose(bool disposing)
{
if (_disposed) return;
try
{
if (disposing)
_extra?.Dispose(); // 釋放衍生類受控資源
// 釋放衍生類非受控資源...
}
finally
{
_disposed = true;
base.Dispose(disposing); // 必須呼叫基類 Dispose
}
}
}
| 錯誤寫法 | 問題 | 修正 |
|---|---|---|
| 實作 IDisposable 但無 Finalize,且未呼叫 SuppressFinalize | 若開發者忘記 Dispose,資源永不釋放 | 加入 Finalize 保底,Dispose 中呼叫 SuppressFinalize |
| Finalize 中存取受控物件 (如 _stream.Close()) | 受控物件可能已被 GC 回收,擲回 NullReferenceException | Finalize 僅釋放非受控資源 (disposing=false) |
| 類別 B 包含實作 IDisposable 的類別 A,但 B 未實作 IDisposable | A 的資源無法被正確釋放 | B 亦實作 IDisposable,其 Dispose 中呼叫 A.Dispose() |
| using 區塊外仍使用已 Dispose 物件 | 存取已釋放資源導致例外 | 設計明確的狀態檢查,或拋擲 ObjectDisposedException |
.NET 2.0+ 提供 SafeHandle 衍生類別 (SafeFileHandle, SafeRegistryHandle 等),內建 Finalize 保護、參考計數、關鍵 Finalization,建議優先使用:
public class ModernWrapper : IDisposable
{
private SafeFileHandle _handle;
private bool _disposed;
public ModernWrapper(string path)
{
_handle = File.OpenHandle(path, FileMode.Open); // .NET 6+ 回傳 SafeFileHandle
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
_handle?.Dispose(); // SafeHandle 內建 Finalize,無需手動 P/Invoke
_disposed = true;
}
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
~ModernWrapper() => Dispose(false);
}
遵循此模式,您的 .NET 應用程式將具備可預測的資源生命週期、優異的記憶體效能與生產環境穩定性。