你应该实现IDisposable.Dispose(),使它永远不会抛出?
对于C ++(析构函数)中的等价机制,build议是通常不应抛出任何exception 。 这主要是因为这样做,你可能会终止你的过程,这只是一个很好的策略。
在.NET中的等效scheme…
- 抛出第一个exception
- 作为第一个exception的结果,finally块被执行
- finally块调用一个Dispose()方法
- Dispose()方法会抛出第二个exception
…你的过程不会立即终止。 但是,您将失去信息,因为.NET不会将第一个exceptionreplace为第二个exception。 因此调用堆栈上的catch块将永远不会看到第一个exception。 但是,人们通常对第一个例外更感兴趣,因为这通常会给出更好的线索,说明为什么事情会出错。
由于.NET缺乏一种机制来检测是否正在执行代码,而exception处于挂起状态,所以IDisposable可以实现的实际上只有两种select:
- 始终吞下Dispose()中发生的所有exception。 不好,因为你最终可能会吞下OutOfMemoryException,ExecutionEngineException等,我通常宁愿让它们在没有另一个exception已经挂起的情况下拆除进程。
- 让所有exception传播出Dispose()。 不好,因为您可能会失去有关问题根源的信息,请参阅上文。
那么,这两个邪恶中的哪一个呢? 有没有更好的办法?
编辑 :澄清,我不是主动抛出exception从Dispose()或不,我正在谈论让由Dispose()调用的方法抛出的exception传播出来的Dispose()或不,例如:
using System; using System.Net.Sockets; public sealed class NntpClient : IDisposable { private TcpClient tcpClient; public NntpClient(string hostname, int port) { this.tcpClient = new TcpClient(hostname, port); } public void Dispose() { // Should we implement like this or leave away the try-catch? try { this.tcpClient.Close(); // Let's assume that this might throw } catch { } } }
我认为在这种情况下,吞咽是两个弊端中较小的一个,因为最好是提高原始的 Exception
– 告诫: 除非干净地处理失败本身是非常重要的(也许如果一个TransactionScope
不能处理,因为这可能表示回滚失败)。
在这里看到更多的想法 – 包括一个包装/扩展方法的想法:
using(var foo = GetDodgyDisposableObject().Wrap()) { foo.BaseObject.SomeMethod(); foo.BaseObject.SomeOtherMethod(); // etc } // now exits properly even if Dispose() throws
当然,你也可以做一些奇怪的事情,你用原来的和第二个( Dispose()
)exception重新抛出一个复合exception – 但是想想:你可以有多个using
块…它很快就变得难以pipe理。 实际上,最初的例外是有趣的。
框架devise指南 (第2版)将其作为(§9.4.1):
AVOID从Dispose(bool)中抛出一个exception,除非在包含进程已被破坏(泄漏,不一致的共享状态等)的严重情况下。
评论[编辑]:
- 有指导方针,而不是硬性规定。 这是一个“避免”而不是“不要”的指导方针。 正如所指出的(在评论中),框架在地方打破了这个(和其他)准则。 诀窍是知道什么时候打破一个准则。 这在很多方面都是熟练人员和主人之间的区别。
- 如果清理的某些部分可能失败,那么应该提供一个Close方法来抛出exception,这样调用者可以处理它们。
- 如果你正在遵循dispose模式(如果types直接包含一些非托pipe资源,那么你可能会从终结器调用
Dispose(bool)
从终结器中抛出是一个坏主意,并会阻止其他对象被终结。
我的观点:从Dispose中逃脱出来的例外只应该像指南那样,是足够灾难性的,以至于在当前的过程中不可能有更多可靠的function。
Dispose
应该被devise来完成它的目的,处理对象。 这个任务是安全的,大部分时间不会抛出exception 。 如果您发现自己抛弃Dispose
exception,则应该多加考虑,看看您是否在做太多东西。 除此之外,我认为Dispose
应该像所有其他方法一样对待:处理是否可以使用它,让它冒泡,如果你不能。
编辑:对于指定的例子,我会写代码,以便我的代码不会导致exception,但清除TcpClient
可能会导致一个exception,这应该是有效的传播在我看来(或处理和rethrow作为一个更多genericsexception,就像任何方法):
public void Dispose() { if (tcpClient != null) tcpClient.Close(); }
然而,就像任何方法一样,如果你知道tcpClient.Close()
可能会抛出一个应该被忽略的exception(无关紧要),或者应该由另一个exception对象来表示,你可能想要捕获它。
释放资源应该是一个“安全”的操作 – 毕竟我怎么能从不能释放资源中恢复呢? 所以从Dispose中抛出一个exception是没有意义的。
但是,如果我发现内部处置程序状态已损坏,最好抛出exception,然后吞下它,它现在更好地粉碎,然后继续运行,并产生不正确的结果。
这太糟糕了,微软并没有提供一个Dispose的Exception参数,打算把它作为一个InnerException包装,以防处理本身抛出一个exception。 可以肯定的是,有效地使用这样一个参数需要使用一个C#不支持的exceptionfilter块,但也许这样的参数的存在可能会促使C#devise者提供这样的特性? 我希望看到的一个很好的变化是在Finally块中增加一个Exception“参数”,例如
最后exception例如:/ /在C# 最后作为例外在VB中
这将像一个正常的最后一块,除非'ex'将是空的/ Nothing如果'尝试'运行完成,或者如果没有,将保留抛出的exception。 太糟糕了,没有办法使现有的代码使用这种function。
我可能会使用日志来捕捉有关第一个exception的细节,然后允许第二个exception被引发。
从Dispose
方法中传播或吞下exception有很多种策略,可能是基于是否还从主逻辑中抛出了一个无用的exception。 最好的解决scheme是根据具体要求将决定留给呼叫者。 我已经实现了一个通用的扩展方法,这样做,提供:
- 默认
using
传播Dispose
exception的语义 - 马克·格雷韦尔的build议总是吞咽
Dispose
例外 - maxyfc只能吞咽的替代方法当主逻辑发生exception时会
Dispose
exception,否则将会丢失 - Daniel Chambers将多个exception包装成一个
AggregateException
- 总是将所有exception包装成一个
AggregateException
(类似于Task.Wait
)
这是我的扩展方法:
/// <summary> /// Provides extension methods for the <see cref="IDisposable"/> interface. /// </summary> public static class DisposableExtensions { /// <summary> /// Executes the specified action delegate using the disposable resource, /// then disposes of the said resource by calling its <see cref="IDisposable.Dispose()"/> method. /// </summary> /// <typeparam name="TDisposable">The type of the disposable resource to use.</typeparam> /// <param name="disposable">The disposable resource to use.</param> /// <param name="action">The action to execute using the disposable resource.</param> /// <param name="strategy"> /// The strategy for propagating or swallowing exceptions thrown by the <see cref="IDisposable.Dispose"/> method. /// </param> /// <exception cref="ArgumentNullException"><paramref name="disposable"/> or <paramref name="action"/> is <see langword="null"/>.</exception> public static void Using<TDisposable>(this TDisposable disposable, Action<TDisposable> action, DisposeExceptionStrategy strategy) where TDisposable : IDisposable { ArgumentValidate.NotNull(disposable, nameof(disposable)); ArgumentValidate.NotNull(action, nameof(action)); ArgumentValidate.IsEnumDefined(strategy, nameof(strategy)); Exception mainException = null; try { action(disposable); } catch (Exception exception) { mainException = exception; throw; } finally { try { disposable.Dispose(); } catch (Exception disposeException) { switch (strategy) { case DisposeExceptionStrategy.Propagate: throw; case DisposeExceptionStrategy.Swallow: break; // swallow exception case DisposeExceptionStrategy.Subjugate: if (mainException == null) throw; break; // otherwise swallow exception case DisposeExceptionStrategy.AggregateMultiple: if (mainException != null) throw new AggregateException(mainException, disposeException); throw; case DisposeExceptionStrategy.AggregateAlways: if (mainException != null) throw new AggregateException(mainException, disposeException); throw new AggregateException(disposeException); } } if (mainException != null && strategy == DisposeExceptionStrategy.AggregateAlways) throw new AggregateException(mainException); } } }
这些是实施的战略:
/// <summary> /// Identifies the strategy for propagating or swallowing exceptions thrown by the <see cref="IDisposable.Dispose"/> method /// of an <see cref="IDisposable"/> instance, in conjunction with exceptions thrown by the main logic. /// </summary> /// <remarks> /// This enumeration is intended to be used from the <see cref="DisposableExtensions.Using"/> extension method. /// </remarks> public enum DisposeExceptionStrategy { /// <summary> /// Propagates any exceptions thrown by the <see cref="IDisposable.Dispose"/> method. /// If another exception was already thrown by the main logic, it will be hidden and lost. /// This behaviour is consistent with the standard semantics of the <see langword="using"/> keyword. /// </summary> /// <remarks> /// <para> /// According to Section 8.10 of the C# Language Specification (version 5.0): /// </para> /// <blockquote> /// If an exception is thrown during execution of a <see langword="finally"/> block, /// and is not caught within the same <see langword="finally"/> block, /// the exception is propagated to the next enclosing <see langword="try"/> statement. /// If another exception was in the process of being propagated, that exception is lost. /// </blockquote> /// </remarks> Propagate, /// <summary> /// Always swallows any exceptions thrown by the <see cref="IDisposable.Dispose"/> method, /// regardless of whether another exception was already thrown by the main logic or not. /// </summary> /// <remarks> /// This strategy is presented by Marc Gravell in /// <see href="http://blog.marcgravell.com/2008/11/dontdontuse-using.html">don't(don't(use using))</see>. /// </remarks> Swallow, /// <summary> /// Swallows any exceptions thrown by the <see cref="IDisposable.Dispose"/> method /// if and only if another exception was already thrown by the main logic. /// </summary> /// <remarks> /// This strategy is suggested in the first example of the Stack Overflow question /// <see href="https://stackoverflow.com/q/1654487/1149773">Swallowing exception thrown in catch/finally block</see>. /// </remarks> Subjugate, /// <summary> /// Wraps multiple exceptions, when thrown by both the main logic and the <see cref="IDisposable.Dispose"/> method, /// into an <see cref="AggregateException"/>. If just one exception occurred (in either of the two), /// the original exception is propagated. /// </summary> /// <remarks> /// This strategy is implemented by Daniel Chambers in /// <see href="http://www.digitallycreated.net/Blog/51/c%23-using-blocks-can-swallow-exceptions">C# Using Blocks can Swallow Exceptions</see> /// </remarks> AggregateMultiple, /// <summary> /// Always wraps any exceptions thrown by the main logic and/or the <see cref="IDisposable.Dispose"/> method /// into an <see cref="AggregateException"/>, even if just one exception occurred. /// </summary> /// <remarks> /// This strategy is similar to behaviour of the <see cref="Task.Wait()"/> method of the <see cref="Task"/> class /// and the <see cref="Task{TResult}.Result"/> property of the <see cref="Task{TResult}"/> class: /// <blockquote> /// Even if only one exception is thrown, it is still wrapped in an <see cref="AggregateException"/> exception. /// </blockquote> /// </remarks> AggregateAlways, }
样品使用:
new FileStream(Path.GetTempFileName(), FileMode.Create) .Using(strategy: DisposeExceptionStrategy.Subjugate, action: fileStream => { // Access fileStream here fileStream.WriteByte(42); throw new InvalidOperationException(); }); // Any Dispose() exceptions will be swallowed due to the above InvalidOperationException
更新 :如果你需要支持返回值和/或asynchronous的委托,那么你可以使用这些重载:
/// <summary> /// Provides extension methods for the <see cref="IDisposable"/> interface. /// </summary> public static class DisposableExtensions { /// <summary> /// Executes the specified action delegate using the disposable resource, /// then disposes of the said resource by calling its <see cref="IDisposable.Dispose()"/> method. /// </summary> /// <typeparam name="TDisposable">The type of the disposable resource to use.</typeparam> /// <param name="disposable">The disposable resource to use.</param> /// <param name="strategy"> /// The strategy for propagating or swallowing exceptions thrown by the <see cref="IDisposable.Dispose"/> method. /// </param> /// <param name="action">The action delegate to execute using the disposable resource.</param> public static void Using<TDisposable>(this TDisposable disposable, DisposeExceptionStrategy strategy, Action<TDisposable> action) where TDisposable : IDisposable { ArgumentValidate.NotNull(disposable, nameof(disposable)); ArgumentValidate.NotNull(action, nameof(action)); ArgumentValidate.IsEnumDefined(strategy, nameof(strategy)); disposable.Using(strategy, disposableInner => { action(disposableInner); return true; // dummy return value }); } /// <summary> /// Executes the specified function delegate using the disposable resource, /// then disposes of the said resource by calling its <see cref="IDisposable.Dispose()"/> method. /// </summary> /// <typeparam name="TDisposable">The type of the disposable resource to use.</typeparam> /// <typeparam name="TResult">The type of the return value of the function delegate.</typeparam> /// <param name="disposable">The disposable resource to use.</param> /// <param name="strategy"> /// The strategy for propagating or swallowing exceptions thrown by the <see cref="IDisposable.Dispose"/> method. /// </param> /// <param name="func">The function delegate to execute using the disposable resource.</param> /// <returns>The return value of the function delegate.</returns> public static TResult Using<TDisposable, TResult>(this TDisposable disposable, DisposeExceptionStrategy strategy, Func<TDisposable, TResult> func) where TDisposable : IDisposable { ArgumentValidate.NotNull(disposable, nameof(disposable)); ArgumentValidate.NotNull(func, nameof(func)); ArgumentValidate.IsEnumDefined(strategy, nameof(strategy)); #pragma warning disable 1998 var dummyTask = disposable.UsingAsync(strategy, async (disposableInner) => func(disposableInner)); #pragma warning restore 1998 return dummyTask.GetAwaiter().GetResult(); } /// <summary> /// Executes the specified asynchronous delegate using the disposable resource, /// then disposes of the said resource by calling its <see cref="IDisposable.Dispose()"/> method. /// </summary> /// <typeparam name="TDisposable">The type of the disposable resource to use.</typeparam> /// <param name="disposable">The disposable resource to use.</param> /// <param name="strategy"> /// The strategy for propagating or swallowing exceptions thrown by the <see cref="IDisposable.Dispose"/> method. /// </param> /// <param name="asyncFunc">The asynchronous delegate to execute using the disposable resource.</param> /// <returns>A task that represents the asynchronous operation.</returns> public static Task UsingAsync<TDisposable>(this TDisposable disposable, DisposeExceptionStrategy strategy, Func<TDisposable, Task> asyncFunc) where TDisposable : IDisposable { ArgumentValidate.NotNull(disposable, nameof(disposable)); ArgumentValidate.NotNull(asyncFunc, nameof(asyncFunc)); ArgumentValidate.IsEnumDefined(strategy, nameof(strategy)); return disposable.UsingAsync(strategy, async (disposableInner) => { await asyncFunc(disposableInner); return true; // dummy return value }); } /// <summary> /// Executes the specified asynchronous function delegate using the disposable resource, /// then disposes of the said resource by calling its <see cref="IDisposable.Dispose()"/> method. /// </summary> /// <typeparam name="TDisposable">The type of the disposable resource to use.</typeparam> /// <typeparam name="TResult">The type of the return value of the asynchronous function delegate.</typeparam> /// <param name="disposable">The disposable resource to use.</param> /// <param name="strategy"> /// The strategy for propagating or swallowing exceptions thrown by the <see cref="IDisposable.Dispose"/> method. /// </param> /// <param name="asyncFunc">The asynchronous function delegate to execute using the disposable resource.</param> /// <returns> /// A task that represents the asynchronous operation. /// The task result contains the return value of the asynchronous function delegate. /// </returns> public static async Task<TResult> UsingAsync<TDisposable, TResult>(this TDisposable disposable, DisposeExceptionStrategy strategy, Func<TDisposable, Task<TResult>> asyncFunc) where TDisposable : IDisposable { ArgumentValidate.NotNull(disposable, nameof(disposable)); ArgumentValidate.NotNull(asyncFunc, nameof(asyncFunc)); ArgumentValidate.IsEnumDefined(strategy, nameof(strategy)); Exception mainException = null; try { return await asyncFunc(disposable); } catch (Exception exception) { mainException = exception; throw; } finally { try { disposable.Dispose(); } catch (Exception disposeException) { switch (strategy) { case DisposeExceptionStrategy.Propagate: throw; case DisposeExceptionStrategy.Swallow: break; // swallow exception case DisposeExceptionStrategy.Subjugate: if (mainException == null) throw; break; // otherwise swallow exception case DisposeExceptionStrategy.AggregateMultiple: if (mainException != null) throw new AggregateException(mainException, disposeException); throw; case DisposeExceptionStrategy.AggregateAlways: if (mainException != null) throw new AggregateException(mainException, disposeException); throw new AggregateException(disposeException); } } if (mainException != null && strategy == DisposeExceptionStrategy.AggregateAlways) throw new AggregateException(mainException); } } }
这里有一种方法可以相当干净地捕获由using
或Dispose
的内容抛出的exception。
原始码:
using (var foo = new DisposableFoo()) { codeInUsing(); }
然后这里是代码,如果codeInUsing()
抛出或foo.Dispose()
抛出或抛出foo.Dispose()
,让你看到第一个exception(有时包装为一个InnerExeption,取决于):
var foo = new DisposableFoo(); Helpers.DoActionThenDisposePreservingActionException( () => { codeInUsing(); }, foo);
这不是很好,但不是太糟糕。
这是实现这个的代码。 我已经设置好了, 只有在debugging器没有连接的情况下才能正常工作,因为当debugging器被连接时,我更担心它会在第一个exception处于正确的位置。 您可以根据需要进行修改。
public static void DoActionThenDisposePreservingActionException(Action action, IDisposable disposable) { bool exceptionThrown = true; Exception exceptionWhenNoDebuggerAttached = null; bool debuggerIsAttached = Debugger.IsAttached; ConditionalCatch( () => { action(); exceptionThrown = false; }, (e) => { exceptionWhenNoDebuggerAttached = e; throw new Exception("Catching exception from action(), see InnerException", exceptionWhenNoDebuggerAttached); }, () => { Exception disposeExceptionWhenExceptionAlreadyThrown = null; ConditionalCatch( () => { disposable.Dispose(); }, (e) => { disposeExceptionWhenExceptionAlreadyThrown = e; throw new Exception("Caught exception in Dispose() while unwinding for exception from action(), see InnerException for action() exception", exceptionWhenNoDebuggerAttached); }, null, exceptionThrown && !debuggerIsAttached); }, !debuggerIsAttached); } public static void ConditionalCatch(Action tryAction, Action<Exception> conditionalCatchAction, Action finallyAction, bool doCatch) { if (!doCatch) { try { tryAction(); } finally { if (finallyAction != null) { finallyAction(); } } } else { try { tryAction(); } catch (Exception e) { if (conditionalCatchAction != null) { conditionalCatchAction(e); } } finally { if (finallyAction != null) { finallyAction(); } } } }