¿Debería implementar IDisposable.Dispose () para que nunca se ejecute?

Para el mecanismo equivalente en C ++ (el destructor), el consejo es que generalmente no debería arrojar ninguna excepción . Esto se debe principalmente a que al hacerlo puede terminar su proceso, lo cual es muy rara vez una buena estrategia.

En el escenario equivalente en .NET …

  1. Se lanza una primera excepción
  2. Un bloque finally se ejecuta como resultado de la primera excepción
  3. El bloque finally llama a un método Dispose ()
  4. El método Dispose () arroja una segunda excepción

… su proceso no finaliza inmediatamente. Sin embargo, pierde información porque .NET reemplaza sin misterios la primera excepción con la segunda. Un bloque catch en algún lugar de la stack de llamadas nunca verá la primera excepción. Sin embargo, uno suele estar más interesado en la primera excepción porque normalmente proporciona mejores pistas sobre por qué las cosas empezaron a ir mal.

Como .NET carece de un mecanismo para detectar si el código se está ejecutando mientras hay una excepción pendiente, parece que en realidad solo hay dos opciones para implementar IDisposable:

  • Siempre trague todas las excepciones que ocurran dentro de Dispose (). No es bueno, ya que también podría terminar ingiriendo OutOfMemoryException, ExecutionEngineException, etc., que normalmente preferiría dejar de procesar cuando ocurran sin que haya otra excepción pendiente.
  • Deje que todas las excepciones se propaguen desde Dispose (). No es bueno, ya que puede perder información sobre la causa raíz de un problema, consulte más arriba.

Entonces, ¿cuál es el menor de los dos males? ¿Hay una mejor manera?

EDITAR : Para aclarar, no estoy hablando de lanzar activamente excepciones de Dispose () o no, estoy hablando de permitir que las excepciones lanzadas por los métodos llamados por Dispose () se propaguen fuera de Dispose () o no, por ejemplo:

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 { } } } 

Yo diría que tragar es el menor de los dos males en este escenario, ya que es mejor plantear la Exception original : advertencia: a menos que , tal vez, la falta de disponer limpiamente sea en sí misma bastante crítica (tal vez si un TransactionScope no pudiera deshacerse , ya que eso podría indicar una falla de reversión).

Consulte aquí para obtener más ideas sobre esto, incluida una idea de método de envoltura / extensión:

 using(var foo = GetDodgyDisposableObject().Wrap()) { foo.BaseObject.SomeMethod(); foo.BaseObject.SomeOtherMethod(); // etc } // now exits properly even if Dispose() throws 

Por supuesto, también podrías hacer algo extraño cuando vuelvas a lanzar una excepción compuesta con la excepción original y la segunda ( Dispose() ), pero piensa: podrías tener múltiples bloques de using … se volvería rápidamente inmanejable. En realidad, la excepción original es la interesante.

Las Pautas de diseño del marco ( edición) tienen esto como (§9.4.1):

EVITE lanzar una excepción desde Dispose (bool), excepto en situaciones críticas en las que el proceso que lo contiene se ha dañado (fugas, estado compartido inconsistente, etc.).

Comentario [Editar]:

  • Hay pautas, no reglas duras. Y esta es una directriz de “EVITAR” y no de “NO HACER”. Como se señaló (en comentarios), el Marco rompe esta (y otras) pautas en algunos lugares. El truco es saber cuándo romper una pauta. Eso, de muchas maneras, es la diferencia entre un Oficial y un Maestro.
  • Si alguna parte de la limpieza puede fallar, entonces debería proporcionarse un método Close que genere excepciones para que la persona que llama pueda manejarlas.
  • Si está siguiendo el patrón de disposición (y debe serlo si el tipo contiene directamente algún recurso no gestionado), el Dispose(bool) puede ser llamado desde el finalizador, lanzar desde un finalizador es una mala idea y bloqueará la finalización de otros objetos .

Mi punto de vista: las excepciones que se escapan de Dispose deben ser solo aquellas, como en la guía, lo suficientemente catastróficas como para que no sea posible otra función confiable del proceso actual.

Dispose debería diseñarse para cumplir su propósito, eliminar el objeto. Esta tarea es segura y no arroja excepciones la mayor parte del tiempo . Si te ves arrojando excepciones de Dispose , probablemente deberías pensarlo dos veces para ver si estás haciendo demasiadas cosas en él. Además de eso, creo que Dispose debe tratarse como todos los demás métodos: manipular si puedes hacer algo con él, dejarlo burbujear si no puedes.

EDITAR: para el ejemplo especificado, escribiría el código para que mi código no cause una excepción, pero borrar el TcpClient podría causar una excepción, que debería ser válida para propagar en mi opinión (o para manejar y volver a lanzar como un excepción genérica, como cualquier método):

 public void Dispose() { if (tcpClient != null) tcpClient.Close(); } 

Sin embargo, al igual que con cualquier método, si sabe que tcpClient.Close() podría arrojar una excepción que debe ignorarse (no importa) o debería representarse con otro objeto de excepción, es posible que desee atraparlo.

La liberación de recursos debe ser una operación “segura”; después de todo, ¿cómo puedo recuperarme si no puedo liberar un recurso? así que lanzar una excepción desde Dispose simplemente no tiene sentido.

Sin embargo, si descubro en el interior de Dispose que el estado del progtwig está dañado, es mejor lanzar la excepción que tragar, es mejor aplastar ahora para continuar y producir resultados incorrectos.

Es una lástima que Microsoft no haya proporcionado un parámetro de Excepción para Dispose, con la intención de que esté envuelto como una InnerException en caso de que la propia eliminación arroje una excepción. Para estar seguro, el uso efectivo de dicho parámetro requeriría el uso de un bloque de filtro de excepción, que C # no admite, pero tal vez la existencia de dicho parámetro podría haber motivado a los diseñadores de C # a proporcionar dicha característica. Una buena variación que me gustaría ver sería la adición de un “parámetro” de excepción a un bloque Finally, por ejemplo

   finalmente excepción ex: // en C #
   Finalmente Ex como excepción 'en VB

que se comportaría como un bloque de Finally normal excepto que ‘ex’ sería nulo / Nothing si el ‘Try’ se ejecutó hasta su finalización, o mantendría la excepción lanzada si no lo hiciera. Lástima que no hay forma de hacer que el código existente use tal característica.

Probablemente usaría el registro para capturar detalles acerca de la primera excepción, luego permitiría que surgiera la segunda excepción.

Hay varias estrategias para propagar o tragar excepciones del método Dispose , posiblemente en función de si también se lanzó una excepción sin manos de la lógica principal. La mejor solución sería dejar la decisión a la persona que llama, según sus requisitos específicos. Implementé un método de extensión genérico que hace esto, ofreciendo:

  • el predeterminado using semántica de propagar Dispose excepciones
  • La sugerencia de Marc Gravell de siempre tragar Dispose excepciones
  • La alternativa de maxyfc de solo tragar Elimina las excepciones cuando hay una excepción de la lógica principal que de otro modo se perdería
  • El enfoque de Daniel Chambers de envolver múltiples excepciones en una excepción AggregateException
  • un enfoque similar que siempre envuelve todas las excepciones en una AggregateException (como Task.Wait hace)

Este es mi método de extensión:

 ///  /// Provides extension methods for the  interface. ///  public static class DisposableExtensions { ///  /// Executes the specified action delegate using the disposable resource, /// then disposes of the said resource by calling its  method. ///  /// The type of the disposable resource to use. /// The disposable resource to use. /// The action to execute using the disposable resource. ///  /// The strategy for propagating or swallowing exceptions thrown by the  method. ///  ///  or  is . public static void Using(this TDisposable disposable, Action 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); } } } 

Estas son las estrategias implementadas:

 ///  /// Identifies the strategy for propagating or swallowing exceptions thrown by the  method /// of an  instance, in conjunction with exceptions thrown by the main logic. ///  ///  /// This enumeration is intended to be used from the  extension method. ///  public enum DisposeExceptionStrategy { ///  /// Propagates any exceptions thrown by the  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  keyword. ///  ///  ///  /// According to Section 8.10 of the C# Language Specification (version 5.0): ///  /// 
/// If an exception is thrown during execution of a block, /// and is not caught within the same block, /// the exception is propagated to the next enclosing statement. /// If another exception was in the process of being propagated, that exception is lost. ///
///
Propagate, /// /// Always swallows any exceptions thrown by the method, /// regardless of whether another exception was already thrown by the main logic or not. /// /// /// This strategy is presented by Marc Gravell in /// don't(don't(use using)). /// Swallow, /// /// Swallows any exceptions thrown by the method /// if and only if another exception was already thrown by the main logic. /// /// /// This strategy is suggested in the first example of the Stack Overflow question /// Swallowing exception thrown in catch/finally block. /// Subjugate, /// /// Wraps multiple exceptions, when thrown by both the main logic and the method, /// into an . If just one exception occurred (in either of the two), /// the original exception is propagated. /// /// /// This strategy is implemented by Daniel Chambers in /// C# Using Blocks can Swallow Exceptions /// AggregateMultiple, /// /// Always wraps any exceptions thrown by the main logic and/or the method /// into an , even if just one exception occurred. /// /// /// This strategy is similar to behaviour of the method of the class /// and the property of the class: ///
/// Even if only one exception is thrown, it is still wrapped in an exception. ///
///
AggregateAlways, }

Uso de muestra:

 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 

Actualización : si necesita admitir delegates que devuelven valores y / o son asíncronos, puede usar estas sobrecargas:

 ///  /// Provides extension methods for the  interface. ///  public static class DisposableExtensions { ///  /// Executes the specified action delegate using the disposable resource, /// then disposes of the said resource by calling its  method. ///  /// The type of the disposable resource to use. /// The disposable resource to use. ///  /// The strategy for propagating or swallowing exceptions thrown by the  method. ///  /// The action delegate to execute using the disposable resource. public static void Using(this TDisposable disposable, DisposeExceptionStrategy strategy, Action 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 }); } ///  /// Executes the specified function delegate using the disposable resource, /// then disposes of the said resource by calling its  method. ///  /// The type of the disposable resource to use. /// The type of the return value of the function delegate. /// The disposable resource to use. ///  /// The strategy for propagating or swallowing exceptions thrown by the  method. ///  /// The function delegate to execute using the disposable resource. /// The return value of the function delegate. public static TResult Using(this TDisposable disposable, DisposeExceptionStrategy strategy, Func 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 restre 1998 return dummyTask.GetAwaiter().GetResult(); } ///  /// Executes the specified asynchronous delegate using the disposable resource, /// then disposes of the said resource by calling its  method. ///  /// The type of the disposable resource to use. /// The disposable resource to use. ///  /// The strategy for propagating or swallowing exceptions thrown by the  method. ///  /// The asynchronous delegate to execute using the disposable resource. /// A task that represents the asynchronous operation. public static Task UsingAsync(this TDisposable disposable, DisposeExceptionStrategy strategy, Func 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 }); } ///  /// Executes the specified asynchronous function delegate using the disposable resource, /// then disposes of the said resource by calling its  method. ///  /// The type of the disposable resource to use. /// The type of the return value of the asynchronous function delegate. /// The disposable resource to use. ///  /// The strategy for propagating or swallowing exceptions thrown by the  method. ///  /// The asynchronous function delegate to execute using the disposable resource. ///  /// A task that represents the asynchronous operation. /// The task result contains the return value of the asynchronous function delegate. ///  public static async Task UsingAsync(this TDisposable disposable, DisposeExceptionStrategy strategy, Func> 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); } } } 

Aquí hay una manera de agarrar bastante limpiamente cualquier excepción arrojada por el contenido del using o el Dispose .

Código original:

 using (var foo = new DisposableFoo()) { codeInUsing(); } 

Luego, aquí está el código que arrojará si codeInUsing() throws o foo.Dispose() throws o both throw, y le permite ver la primera excepción (a veces envuelto como InnerExeption, dependiendo):

 var foo = new DisposableFoo(); Helpers.DoActionThenDisposePreservingActionException( () => { codeInUsing(); }, foo); 

No es genial, pero no está mal.

Aquí está el código para implementar esto. Lo tengo configurado para que solo funcione como se describe cuando el depurador no está conectado, porque cuando se adjunta el depurador me preocupa más que se rompa en el lugar correcto en la primera excepción. Puede modificar según sea necesario.

 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 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(); } } } }