必须实现IDisposable仅当直接持有非托管资源或控制IDisposable托管对象生命周期;Dispose(bool disposing)中true时可释放托管和非托管资源,false时仅释放非托管资源;需用_disposed标志、置空句柄、调用GC.SuppressFinalize。

不需要手写裸 Dispose() 方法,标准写法是带 Dispose(bool disposing) 的双参数模板。 这不是“最佳实践”建议,而是.NET运行时和GC协作的硬性要求——错用会导致资源重复释放、访问已回收对象、句柄泄漏,甚至在高并发下直接崩溃。
什么时候必须实现 IDisposable?
只在两类情况下才需要:你的类直接持有非托管资源(如 IntPtr、SafeHandle、Socket 原生句柄),或拥有并控制一个 IDisposable 托管对象的生命周期(如自己 new 出 FileStream、HttpClient、DbContext 并保存为字段)。
以下情况不需要实现:List<string>、Dictionary<int, object>、普通引用类型字段——GC 会自动回收,加 IDisposable 反而误导调用方。
- 误判高频点:把“用了数据库”等同于“要实现
IDisposable”——实际应由DbContext或DbConnection自己负责,你只是消费它 - 依赖注入场景下,服务注册时已声明
ServiceLifetime(如Scoped),容器会在作用域结束时自动调用Dispose(),你无需手动干预
Dispose(bool disposing) 里到底该放什么?
这个方法是整个模式的核心,disposing 参数不是可选开关,而是执行环境的明确信号:
- 当
disposing == true:表示由用户显式调用Dispose()或using触发,此时可安全调用其他托管对象的Dispose(),也可释放非托管资源 - 当
disposing == false:表示由 GC 的 finalizer 线程调用(即析构函数中触发),此时绝不能访问任何托管对象(它们可能已被回收),只能释放非托管资源(如Marshal.FreeHGlobal、CloseHandle)
常见错误:在 disposing == false 分支里调用 _stream?.Dispose() 或触发事件 —— 运行时抛 ObjectDisposedException 或静默崩溃。
要不要写析构函数(~ClassName())?
95% 的 C# 类不需要析构函数。 它只在你直接分配了非托管内存(如 Marshal.AllocHGlobal)、调用了 Win32 API 获取句柄、或封装了 SafeHandle 子类时才需要。
- 写了析构函数但没非托管资源 → 拖慢 GC,对象多待一轮回收,纯属负优化
- 没写析构函数但有非托管资源 → 资源泄漏风险真实存在,尤其长时间运行服务
- 写了析构函数就必须在
Dispose(true)后调用GC.SuppressFinalize(this),否则 finalizer 仍会被执行,造成二次释放
注意:SafeHandle 及其子类(如 SafeFileHandle)已内置 finalizer,你用它封装非托管句柄后,自己就不用再写析构函数。
容易被忽略的细节和坑
真正出问题的往往不是大逻辑,而是这几个小地方:
-
_disposed标志位必须是private bool,且所有公开方法开头都要检查:if (_disposed) throw new ObjectDisposedException(...) - 非托管资源释放后必须置为无效值(如
_handle = IntPtr.Zero),否则下次调用Dispose()可能因判空失败导致重复释放 - 子类继承可释放类型时,
Dispose(bool)必须是protected virtual,子类重写后第一件事是调用base.Dispose(disposing),顺序不能反 -
using块内抛异常时,Dispose()仍会执行 —— 所以清理逻辑本身不能抛异常,或需用try/catch吞掉底层释放异常(如文件已被删除)
最常漏掉的一行是 GC.SuppressFinalize(this):不加它,即使用户显式调用了 Dispose(),finalizer 仍会排队执行,等于白做。


















