Durante el cambio a los nuevos .NET Core 3 IAsynsDisposable
, me encontré con el siguiente problema.
El núcleo del problema: si DisposeAsync
arroja una excepción, esta excepción oculta cualquier excepción lanzada dentro de await using
-block.
class Program
{
static async Task Main()
{
try
{
await using (var d = new D())
{
throw new ArgumentException("I'm inside using");
}
}
catch (Exception e)
{
Console.WriteLine(e.Message); // prints I'm inside dispose
}
}
}
class D : IAsyncDisposable
{
public async ValueTask DisposeAsync()
{
await Task.Delay(1);
throw new Exception("I'm inside dispose");
}
}
Lo que queda atrapado es la AsyncDispose
excepción-si se lanza, y la excepción desde adentro await using
solo si AsyncDispose
no se lanza.
Sin embargo, preferiría lo contrario: obtener la excepción del await using
bloque si es posible, y DisposeAsync
-excepción solo si el await using
bloque terminó con éxito.
Justificación: Imagine que mi clase D
trabaja con algunos recursos de red y se suscribe a algunas notificaciones remotas. El código interno await using
puede hacer algo mal y fallar el canal de comunicación, luego el código en Dispose que intenta cerrar la comunicación con gracia (por ejemplo, cancelar la suscripción a las notificaciones) también fallará. Pero la primera excepción me da la información real sobre el problema, y la segunda es solo un problema secundario.
En el otro caso, cuando la parte principal se ejecutó y la eliminación falló, el verdadero problema está dentro DisposeAsync
, por lo que la excepción DisposeAsync
es la relevante. Esto significa que suprimir todas las excepciones en el interior DisposeAsync
no debería ser una buena idea.
Sé que existe el mismo problema con el caso no asíncrono: la excepción en finally
anula la excepción en try
, por eso no se recomienda incluir Dispose()
. Pero con las clases de acceso a la red que suprimen las excepciones en los métodos de cierre no se ve nada bien.
Es posible solucionar el problema con el siguiente ayudante:
static class AsyncTools
{
public static async Task UsingAsync<T>(this T disposable, Func<T, Task> task)
where T : IAsyncDisposable
{
bool trySucceeded = false;
try
{
await task(disposable);
trySucceeded = true;
}
finally
{
if (trySucceeded)
await disposable.DisposeAsync();
else // must suppress exceptions
try { await disposable.DisposeAsync(); } catch { }
}
}
}
y úsalo como
await new D().UsingAsync(d =>
{
throw new ArgumentException("I'm inside using");
});
lo cual es un poco feo (y no permite cosas como retornos tempranos dentro del bloque de uso).
¿Existe una buena solución canónica, await using
si es posible? Mi búsqueda en internet no encontró ni siquiera discutir este problema.
Close
método separado por esta misma razón. Probablemente sea aconsejable hacer lo mismo:CloseAsync
intenta cerrar las cosas muy bien y arroja el fracaso.DisposeAsync
simplemente hace lo mejor y falla en silencio.CloseAsync
medio separado significa que debo tomar precauciones adicionales para que funcione. Si lo pongo al final deusing
-block, se omitirá en los primeros retornos, etc. (esto es lo que queremos que suceda) y excepciones (esto es lo que queremos que suceda). Pero la idea parece prometedora.Dispose
siempre ha sido "Las cosas podrían haber salido mal: solo haz tu mejor esfuerzo para mejorar la situación, pero no la empeores", y no veo por quéAsyncDispose
debería ser diferente.DisposeAsync
lo mejor para ordenar pero no tirar es lo correcto. Estaba hablando de devoluciones anticipadas intencionales , donde una devolución anticipada intencional podría omitir por error una llamada aCloseAsync
: esas son las prohibidas por muchos estándares de codificación.Respuestas:
Hay excepciones que desea mostrar (interrumpir la solicitud actual o reducir el proceso), y hay excepciones que su diseño espera que ocurran a veces y puede manejarlas (por ejemplo, vuelva a intentarlo y continúe).
Pero distinguir entre estos dos tipos depende de la persona que realiza la última llamada del código; este es el punto principal de las excepciones, dejar la decisión a la persona que llama.
A veces, la persona que llama dará mayor prioridad a la aparición de la excepción del bloque de código original, y a veces la excepción del
Dispose
. No existe una regla general para decidir cuál debe tener prioridad. El CLR es al menos consistente (como ha notado) entre el comportamiento sincronizado y el no asíncrono.Tal vez sea desafortunado que ahora tengamos
AggregateException
que representar múltiples excepciones, no se puede adaptar para resolver esto. es decir, si una excepción ya está en vuelo y se lanza otra, se combinan en unAggregateException
. Elcatch
mecanismo podría modificarse para que, si escribecatch (MyException)
, capture cualquieraAggregateException
que incluya una excepción de tipoMyException
. Sin embargo, hay varias otras complicaciones derivadas de esta idea, y probablemente sea demasiado arriesgado modificar algo tan fundamental ahora.Podría mejorar su
UsingAsync
para respaldar la devolución anticipada de un valor:fuente
await using
se puede usar estándar (aquí es donde DisposeAsync no arrojará un caso no fatal), y un ayudante comoUsingAsync
es más apropiado (si es probable que DisposeAsync arroje) ? (Por supuesto, tendría que modificarUsingAsync
para que no capte todo a ciegas, pero solo no sea fatal (y no descabellado en el uso de Eric Lippert ).)Quizás ya entiendas por qué sucede esto, pero vale la pena explicarlo. Este comportamiento no es específico para
await using
. También sucedería con unusing
bloque simple . Entonces, mientras digoDispose()
aquí, todo se aplicaDisposeAsync()
también.Un
using
bloque es solo azúcar sintáctico para untry
/finally
bloque, como dice la sección de comentarios de la documentación . Lo que ves sucede porque elfinally
bloque siempre se ejecuta, incluso después de una excepción. Entonces, si ocurre una excepción y no haycatch
bloque, la excepción se pone en espera hastafinally
que se ejecuta el bloque y luego se lanza la excepción. Pero si ocurre una excepciónfinally
, nunca verá la excepción anterior.Puedes ver esto con este ejemplo:
No importa si
Dispose()
oDisposeAsync()
se llama dentro de lafinally
. El comportamiento es el mismo.Mi primer pensamiento es: no tirar
Dispose()
. Pero después de revisar algunos de los códigos propios de Microsoft, creo que depende.Eche un vistazo a su implementación de
FileStream
, por ejemplo. Tanto elDispose()
método sincrónico , y enDisposeAsync()
realidad puede lanzar excepciones. El síncronaDispose()
hace caso omiso de algunas excepciones intencionalmente, pero no todos.Pero creo que es importante tener en cuenta la naturaleza de tu clase. En un
FileStream
, por ejemplo,Dispose()
vaciará el búfer al sistema de archivos. Esa es una tarea muy importante y necesita saber si eso falló . No puedes ignorar eso.Sin embargo, en otros tipos de objetos, cuando llamas
Dispose()
, ya no tienes uso para el objeto. LlamarDispose()
realmente solo significa "este objeto está muerto para mí". Tal vez limpia parte de la memoria asignada, pero la falla no afecta el funcionamiento de su aplicación de ninguna manera. En ese caso, puede decidir ignorar la excepción dentro de suDispose()
.Pero en cualquier caso, si desea distinguir entre una excepción dentro
using
o una excepción que provieneDispose()
, entonces necesita untry
/catch
bloque tanto dentro como fuera de suusing
bloque:O simplemente no podrías usar
using
. Escriba atry
/catch
/finally
block usted mismo, donde detecte cualquier excepción enfinally
:fuente
catch
interior delusing
bloque no ayudaría porque, por lo general, el manejo de excepciones se realiza en algún lugar lejos delusing
bloque en sí, por lo que nousing
es muy práctico manejarlo dentro . Sobre el uso de nousing
, ¿es realmente mejor que la solución propuesta?try
/catch
/finally
bloquear ya que sería claro de inmediato lo que está haciendo sin tener que ir a leer lo queAsyncUsing
está haciendo. También tiene la opción de hacer un regreso temprano. También habrá un costo adicional de CPU para suAwaitUsing
. Sería pequeño, pero está ahí.Dispose()
no se debe tirar porque se llama más de una vez. Las propias implementaciones de Microsoft pueden arrojar excepciones, y por una buena razón, como he mostrado en esta respuesta. Sin embargo, estoy de acuerdo en que debe evitarlo si es posible, ya que nadie esperaría que se lance normalmente.usar es efectivamente el Código de manejo de excepciones (sintaxis de azúcar para probar ... finalmente ... Dispose ()).
Si su código de manejo de excepciones arroja Excepciones, algo está realmente roto.
Cualquier otra cosa que te haya metido allí, ya no importa. El código de manejo de excepciones defectuosas ocultará todas las excepciones posibles, de una forma u otra. El código de manejo de excepciones debe ser fijo, eso tiene prioridad absoluta. Sin eso, nunca obtienes suficientes datos de depuración para el problema real. Veo que se hace mal extremadamente a menudo. Es tan fácil equivocarse como manejar punteros desnudos. Muy a menudo, hay dos artículos sobre el tema temático que enlace, que pueden ayudarlo con cualquier concepción errónea subyacente:
Dependiendo de la clasificación de excepciones, esto es lo que debe hacer si su código de manejo de excepciones / dipose produce una excepción:
Para Fatal, Boneheaded y Vexing, la solución es la misma.
Excepciones exógenas, deben evitarse incluso a un costo serio. Hay una razón por la que todavía utilizamos archivos de registro en lugar de bases de datos de registro para registrar excepciones: las operaciones de base de datos DB son solo una forma de ser propensos a tener problemas exógenos. Los archivos de registro son el único caso, donde ni siquiera me importa si mantiene el controlador de archivo abierto durante todo el tiempo de ejecución.
Si tienes que cerrar una conexión, no te preocupes demasiado por el otro extremo. Manéjelo como lo hace UDP: "Enviaré la información, pero no me importa si la otra parte la obtiene". Desechar es limpiar recursos del lado del cliente en el que está trabajando.
Puedo intentar notificarles. ¿Pero limpiar cosas en el lado del servidor / FS? De eso son responsables sus tiempos de espera y su manejo de excepciones.
fuente
NativeMethods.UnmapViewOfFile();
yNativeMethods.CloseHandle()
. Pero esos son importados desde el exterior. No se verifica ningún valor de retorno o cualquier otra cosa que pueda usarse para obtener una Excepción .NET adecuada en torno a lo que puedan encontrar esos dos. Así que estoy asumiendo firmemente que SqlConnection.Dispose (bool) simplemente no le importa. El | Cerrar es mucho mejor, en realidad le dice al servidor. Antes de que llame disponga.Puede intentar usar AggregateException y modificar su código de la siguiente manera:
https://docs.microsoft.com/ru-ru/dotnet/api/system.aggregateexception?view=netframework-4.8
https://docs.microsoft.com/ru-ru/dotnet/standard/parallel-programming/exception-handling-task-parallel-library
fuente