解構子語法與限制
class ResourceHolder {
private IntPtr _nativeHandle;
~ResourceHolder() { // 語法:~ClassName()
// 僅清理非受控資源
if (_nativeHandle != IntPtr.Zero) {
NativeMethods.CloseHandle(_nativeHandle);
_nativeHandle = IntPtr.Zero;
}
}
}
- 名稱固定為
~ + 類別名稱,不可自訂。
- 無參數、無回傳值、無存取修飾詞、不可多載、不可繼承、不可顯式呼叫。
- 編譯器轉換為
protected override void Finalize()。
非決定性執行特性
| 特性 | 說明 |
| 執行時機 | GC 判定物件無法觸達後,由 Finalizer Thread 排程執行 |
| 順序不保證 | 多個物件解構子執行順序不可預測 |
| 可能不執行 | 進程強制終止、AppDomain 卸載、嚴重錯誤時 |
| 例外處理 | 解構子拋出未捕捉例外 → 進程終止 (NET Framework) 或被吞噬 (.NET Core+) |
| 復活物件 | 解構子中將 this 指派給靜態欄位 → 物件復活,下次 GC 再次最終化 |
為什麼不建議依賴解構子釋放關鍵資源?
- 延遲釋放:資源佔用時間不可控,可能耗盡系統資源 (連線池、檔案鎖)。
- 順序風險:解構子執行時其他受控物件可能已被最終化,存取它們不安全。
- 效能影響:有解構子的物件需進入 Finalization Queue,GC 代數提升較慢,增加記憶體壓力。
標準釋放模式:IDisposable + 解構子雙重保險
public sealed class FileManager : IDisposable {
private FileStream _stream;
private bool _disposed;
public FileManager(string path) => _stream = File.Open(path, FileMode.Open);
// 公開釋放方法:決定性釋放
public void Dispose() {
Dispose(true);
GC.SuppressFinalize(this); // 告訴 GC 不需呼叫解構子
}
// 核心邏輯:disposing=true 來自 Dispose(),false 來自解構子
private void Dispose(bool disposing) {
if (_disposed) return;
if (disposing) {
// 釋放受控資源 (其他 IDisposable 物件)
_stream?.Dispose();
}
// 釋放非受控資源 (無論 disposing)
_stream = null;
_disposed = true;
}
// 解構子:最後防線,僅釋放非受控資源
~FileManager() => Dispose(false);
}
using 陳述式:語法糖保證釋放
// 編譯器展開為 try/finally 區塊
using (var fm = new FileManager("data.txt")) {
fm.Process();
}
// 等同於
var fm = new FileManager("data.txt");
try { fm.Process(); }
finally { fm?.Dispose(); }
現代 .NET 進階釋放模式
- IAsyncDisposable (C# 8/.NET Core 3.0):
await using 非同步釋放。
- SafeHandle 衍生類別:包裝非受控控制代碼,內建最終化邏輯,避免手寫解構子。
- MemoryManager<T>:自訂記憶體管理釋放邏輯。
最佳實務檢核表
- [ ] 實作
IDisposable 而非單獨依賴解構子。
- [ ]
Dispose() 可重複呼叫不拋出例外 (冪等)。
- [ ]
Dispose() 呼叫 GC.SuppressFinalize(this)。
- [ ] 解構子僅清理非受控資源,呼叫
Dispose(false)。
- [ ] 密封類別 (
sealed) 簡化釋放邏輯 (無衍生類別需考慮)。
- [ ] 公開 API 文件註解說明「必須呼叫 Dispose」或「支援 using」。