在 .NET 托管環境中,記憶體由垃圾收集器 (GC) 自動管理,但非托管資源(檔案控制代碼、資料庫連線、網路 Socket、GDI 物件、COM 物件等)必須由開發者明確釋放。本文完整解析解構函式、IDisposable、Dispose Pattern、using 陳述式的關係與正確用法。
| 概念 | 用途 | 呼叫時機 | 決定者 |
|---|---|---|---|
解構函式 ~Class() | 最後防線:釋放非托管資源 | GC 回收物件時 (非確定性) | GC |
| IDisposable.Dispose() | 主動釋放:托管+非托管資源 | 程式碼明確呼叫 (確定性) | 開發者 |
| using 陳述式 | 語法糖:確保 Dispose 被呼叫 | 區塊結束時 (含例外) | 編譯器 |
public class MyClass
{
// 1. 無法指定存取修飾詞 (隱含 protected)
// 2. 無參數、無法多載、無法繼承
// 3. 無法直接呼叫
~MyClass()
{
// 編譯器轉換為:
// protected override void Finalize()
// {
// try { /* 釋放非托管資源 */ }
// finally { base.Finalize(); }
// }
// 僅釋放非托管資源!
// 此時托管物件可能已被回收,不可觸碰
}
}
using System;
public class ResourceHolder : IDisposable
{
// 非托管資源 (IntPtr, SafeHandle, 檔案控制代碼等)
private IntPtr _unmanagedHandle = IntPtr.Zero;
// 托管資源 (實作 IDisposable 的物件)
private FileStream _fileStream;
private Timer _timer;
// 狀態旗標
private bool _disposed = false;
public ResourceHolder(string filePath)
{
// 取得非托管資源
_unmanagedHandle = NativeMethods.OpenFile(filePath);
if (_unmanagedHandle == IntPtr.Zero)
throw new System.ComponentModel.Win32Exception();
// 取得托管資源
_fileStream = new FileStream(filePath, FileMode.Open);
_timer = new Timer(Callback, null, 1000, 1000);
}
// 公開 Dispose:開發者主動呼叫
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // 關鍵:告訴 GC 不需呼叫 Finalize
}
// 核心邏輯:disposing 區分呼叫來源
protected virtual void Dispose(bool disposing)
{
if (_disposed) return; // 避免重複釋放
if (disposing)
{
// === 釋放托管資源 (僅開發者呼叫時) ===
_timer?.Dispose();
_timer = null;
_fileStream?.Dispose();
_fileStream = null;
// 其他 IDisposable 物件...
}
// === 釋放非托管資源 (兩種情況都要) ===
if (_unmanagedHandle != IntPtr.Zero)
{
NativeMethods.CloseHandle(_unmanagedHandle);
_unmanagedHandle = IntPtr.Zero;
}
_disposed = true;
}
// 解構函式:最後防線
~ResourceHolder()
{
// 僅釋放非托管資源
Dispose(false);
}
// 使用範例方法
public void ReadData()
{
if (_disposed) throw new ObjectDisposedException(nameof(ResourceHolder));
// 讀取邏輯...
}
private void Callback(object state) { /* 定時工作 */ }
}
internal static class NativeMethods
{
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
public static extern IntPtr OpenFile(string path);
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
public static extern bool CloseHandle(IntPtr handle);
}
public sealed class SimpleResource : IDisposable
{
private bool _disposed;
private IntPtr _handle;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
// 密封類別無需 virtual、無需 protected
private void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
// 釋放托管資源
}
// 釋放非托管資源
if (_handle != IntPtr.Zero)
{
NativeMethods.CloseHandle(_handle);
_handle = IntPtr.Zero;
}
_disposed = true;
}
~SimpleResource() => Dispose(false);
}
強烈建議:不用手寫 IntPtr + Finalize,改用 SafeHandle 衍生類別,由 Runtime 保證釋放:
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
public class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid
{
public SafeFileHandle(IntPtr handle, bool ownsHandle) : base(ownsHandle)
{
SetHandle(handle);
}
protected override bool ReleaseHandle()
{
return NativeMethods.CloseHandle(handle);
}
}
// 使用
public class ModernResource : IDisposable
{
private SafeFileHandle _handle;
private FileStream _stream;
public void Dispose()
{
_handle?.Dispose(); // SafeHandle 內建 Finalize 保護
_stream?.Dispose();
}
}
using (var resource = new ResourceHolder("data.txt"))
{
resource.ReadData();
} // 編譯器產生 try/finally 確保 Dispose()
public void Process()
{
using var resource = new ResourceHolder("data.txt"); // 方法結束時自動 Dispose
using var stream = File.OpenRead("data.txt");
resource.ReadData();
// 即使拋出例外,兩者都會被 Dispose
}
ResourceHolder resource = new ResourceHolder("data.txt");
try
{
resource.ReadData();
}
finally
{
if (resource != null)
((IDisposable)resource).Dispose();
}
| 錯誤寫法 | 問題 | 正確做法 |
|---|---|---|
| 只實作 ~Finalize(),無 IDisposable | 無法主動釋放,資源佔用過久 | 實作 IDisposable + Dispose Pattern |
| Dispose() 中未呼叫 GC.SuppressFinalize | GC 仍會呼叫 Finalize,效能損耗 | Dispose(true) 後加上 SuppressFinalize |
| Finalize 中觸碰托管物件 | 托管物件可能已回收,存取違規 | Finalize 僅釋放非托管資源 |
| 未檢查 _disposed 旗標 | 重複 Dispose 可能導致例外 | 開頭檢查 if (_disposed) return |
| Dispose 拋出例外 | 破壞 using/finally 保證 | Dispose 內部捕獲並記錄,不向外拋 |
| 實作 IDisposable 但未密封且無 virtual Dispose(bool) | 子類別無法正確擴充釋放邏輯 | 提供 protected virtual Dispose(bool) |
public interface IAsyncDisposable
{
ValueTask DisposeAsync();
}
public class AsyncResource : IAsyncDisposable
{
private HttpClient _client = new HttpClient();
public async ValueTask DisposeAsync()
{
await _client.DisposeAsync(); // 非同步釋放
}
}
// 使用
await using var resource = new AsyncResource();
// ASP.NET Core / .NET Generic Host
services.AddScoped(); // Request 結束自動 Dispose
services.AddSingleton(); // 應用程式結束 Dispose
services.AddTransient(); // 每次建立,需手動管理
// 實作 IAsyncDisposable 的服務會在對應時機自動 DisposeAsync
// 大型陣列 (>85KB) 進入 LOH,GC 成本高
// 使用 ArrayPool 避免頻繁配置
byte[] buffer = ArrayPool.Shared.Rent(100000);
try { /* 使用 buffer */ }
finally { ArrayPool.Shared.Return(buffer); }
資源管理鐵律:誰配置,誰負責釋放。標準流程:
IDisposable + 完整 Dispose PatternSafeHandle 衍生類別包裝非托管控制代碼IAsyncDisposable、ArrayPool、DI 容器生命週期管理記住:GC.Collect() 不是解決資源洩漏的辦法,正確的 Dispose 模式才是。