c# - net - ¿Debería implementar IDisposable.Dispose() para que nunca se ejecute?
override void dispose (8)
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 ...
- Se lanza una primera excepción
- Un bloque finally se ejecuta como resultado de la primera excepción
- El bloque finally llama a un método Dispose ()
- 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 pila 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
{
}
}
}
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 dentro de Dispose que el estado del programa está dañado, es mejor lanzar la excepción y luego tragarla, es mejor aplastar ahora para continuar y producir resultados incorrectos.
Probablemente usaría el registro para capturar detalles acerca de la primera excepción, luego permitiría que surgiera la segunda excepción.
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.
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 debe representarse con otro objeto de excepción, es posible que desee atraparlo.
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
finally Exception ex: // In C# Finally Ex as Exception '' In 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.
Las Pautas de diseño del marco ( 2ª 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.
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 permiten 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<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();
}
}
}
}
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 propagarDispose
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
(comoTask.Wait
hace)
Este es mi método de extensión:
/// <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);
}
}
}
Estas son las estrategias implementadas:
/// <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 question
/// <see href="https://.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,
}
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 delegados que devuelven valores y / o son asíncronos, puede usar estas sobrecargas:
/// <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);
}
}
}