在catch块中清理Disposable对象是否合理?有无更规范的实现方式?
原有方案存在的问题
- 无差别释放所有传入对象:无法确定异常抛出时a和b是否已经完成加载、甚至是否已经开始初始化,直接统一调用Dispose可能误销毁本来可以正常使用的对象(比如异常是在reader.Dispose阶段抛出的,这时候a和b已经加载完成,完全不需要销毁)
- 释放过程存在资源泄漏风险:如果调用
a.Dispose()时本身抛出异常,后续的b.Dispose()就不会执行,导致b持有的原生内存泄漏 - 不符合异常安全设计原则:把资源清理的逻辑耦合在业务调用方,而不是由资源持有类自己保证加载失败时的内部资源清理
LoadFrom内部释放方案的问题
- 破坏对象的预期生命周期:调用
LoadFrom失败后直接销毁对象本身,如果上层代码还持有该对象引用,后续操作会触发ObjectDisposedException,且上层无法感知对象已经被释放 - 无意义的释放调用:如果异常发生在LoadFrom还没分配任何原生资源的阶段,此时调用Dispose完全多余,如果Dispose没有实现幂等还会引发额外问题
更规范的实现方案
首先明确基础设计约定:可释放类型的Dispose方法必须实现幂等,也就是多次调用不会抛出异常、不会产生二次释放等问题,这是.NET中IDisposable实现的标准要求。
方案1:类内部保证异常安全(最推荐)
要求LoadFrom方法满足「要么完全成功,要么不留下任何未释放的内部资源」的异常安全保证,加载过程中如果出现异常,方法内部自行清理本次加载已经分配的资源,不会泄露到上层,上层不需要关心加载失败后的清理逻辑。
示例代码:
public class DisposableTypeA : IDisposable { private IntPtr _nativePtr = IntPtr.Zero; // 其他原生资源字段 public void LoadFrom(BinaryReader reader) { IntPtr tempPtr = IntPtr.Zero; try { // 加载过程中先把资源赋值给临时变量,不要直接修改实例字段 tempPtr = AllocateNativeMemory(reader.ReadInt32()); // 执行其他加载步骤,所有步骤都成功再赋值给实例字段 _nativePtr = tempPtr; tempPtr = IntPtr.Zero; } finally { // 只要临时变量不是空,说明加载中途失败,释放临时分配的资源 if (tempPtr != IntPtr.Zero) FreeNativeMemory(tempPtr); } } public void Dispose() { if (_nativePtr != IntPtr.Zero) { FreeNativeMemory(_nativePtr); _nativePtr = IntPtr.Zero; } GC.SuppressFinalize(this); } // 省略析构函数等其他IDisposable标准实现 }
这种实现下上层的LoadData逻辑可以非常简洁,不需要额外的清理逻辑:
static void LoadData(string path, DisposableTypeA a, DisposableTypeB b) { using (BinaryReader reader = new BinaryReader(File.Open(path, FileMode.Open))) { a.LoadFrom(reader); b.LoadFrom(reader); } }
如果加载失败,上层只需要处理异常即可,不会有资源泄漏。
方案2:上层标记加载状态,统一清理
如果无法修改DisposableType的内部实现,可以在上层标记每个对象的加载成功状态,出现异常时只清理已经加载成功的对象,并且单独捕获每个Dispose的异常避免泄漏:
static void LoadData(string path, DisposableTypeA a, DisposableTypeB b) { bool aLoadSuccess = false; bool bLoadSuccess = false; try { using (BinaryReader reader = new BinaryReader(File.Open(path, FileMode.Open))) { a.LoadFrom(reader); aLoadSuccess = true; b.LoadFrom(reader); bLoadSuccess = true; } } catch (Exception e) { Console.WriteLine(e.ToString()); // 单独处理每个Dispose,避免单个Dispose失败导致其他资源泄漏 if (aLoadSuccess) { try { a.Dispose(); } catch { /* 可记录Dispose异常 */ } } if (bLoadSuccess) { try { b.Dispose(); } catch { /* 可记录Dispose异常 */ } } throw; } }
内容的提问来源于stack exchange,提问作者Jake1234
相关产品推荐
相关产品推荐

