应仅在封装非托管资源时使用终结器,且必须配合idisposable;终结器不保证执行时机,是gc异步回调的最后保险,不可替代dispose。

别写终结器,除非你正在封装非托管资源(比如文件句柄、GDI对象、未托管内存指针);写了也得配 IDisposable,否则资源泄漏风险极高。
Finalizer 不是析构函数,它不保证执行时机
很多人看到 ~ClassName() 就以为是 C++ 那种“对象销毁时立刻调用”的析构函数,其实完全不是。Finalizer 是 GC 在回收对象前,**异步插入 Finalize 队列并由专用线程回调**的机制。这意味着:
- 你无法预测它什么时候执行——可能几秒后,也可能程序退出前都不执行
- 如果对象在 GC 第 0 代就被回收,Finalizer 不会触发;只有被提升到第 1 或第 2 代后,且进入 Finalize 队列,才可能被处理
- 若 Finalizer 执行时间过长(比如阻塞 I/O),会拖慢整个 Finalizer 线程,导致后续所有待终结对象积压
-
GC.SuppressFinalize(this)必须在Dispose(true)里调用,否则即使手动释放了资源,Finalizer 还会在某次 GC 中再跑一遍
~ClassName() 实际会被编译成重写 Finalize 方法
C# 编译器把 ~Car() 自动转成如下等效代码:
protected override void Finalize()
{
try
{
// 你写的清理逻辑
}
finally
{
base.Finalize();
}
}
这个转换带来两个关键事实:
- 每个类只能有一个
~ClassName(),因为Finalize是虚方法,不能重载 - 基类的
Finalize总是会被调用(在finally块里),所以派生类终结器执行完后,基类逻辑也会走一遍 - 如果你写了空终结器(
~Car() { }),它仍会入队、被调度、执行空try/finally—— 白占 GC 周期,拖慢回收速度
IDisposable + Finalizer 组合才是安全模式
单独靠 Finalizer 清理非托管资源是危险的。正确做法是实现标准的 Dispose 模式:
- 用户调用
obj.Dispose()→ 立即释放托管+非托管资源,并调用GC.SuppressFinalize(this) - GC 回收时发现没被
Suppress→ 才执行 Finalizer(作为“最后保险”) - Finalizer 里只做非托管资源释放(不能访问已可能被 GC 的托管对象),且必须判空(因为
Dispose()可能已清过) - 所有非托管字段建议用
SafeHandle子类包装(如SafeFileHandle),它自带终结逻辑且线程安全,能大幅简化代码
结构体不能有 Finalizer,也不该有
struct 是值类型,分配在栈或内联于其他对象中,不受 GC 管理,因此语言直接禁止定义 ~StructName()。如果你发现某个结构体“需要清理”,说明设计错了——要么该改成 class,要么把资源管理逻辑上提到持有它的引用类型中。常见误用是给 Span<t></t> 或 ReadOnlyMemory<t></t> 类型加终结器,这不仅非法,而且毫无意义。
最常被忽略的一点:Finalizer 不会阻止对象被 GC 标记为可回收,它只是让对象在回收前“多活一个周期”。一旦进入 Finalize 队列,对象就从“可回收”变成“待终结”,但此时它占用的内存依然不可用——相当于人为制造了一次延迟回收,还增加了 GC 工作负担。真要控制资源生命周期,using 和 IDisposable 才是正解。










