2009-11-29 15 views
14

Estamos investigando un patrón de codificación en C# en el que nos gustaría usar una cláusula "using" con una clase especial, cuyo método Dispose() hace cosas diferentes dependiendo de si el cuerpo "using" se salió normalmente o con una excepción .¿Cómo determinar si se está manejando una excepción .NET?

Según entiendo, CLR realiza un seguimiento de la excepción actual que se maneja hasta que se ha consumido por un controlador "catch". Sin embargo, no está del todo claro si esta información está expuesta de alguna manera para que el código acceda. ¿Sabes si es, y si es así, cómo acceder?

Por ejemplo:

using (var x = new MyObject()) 
{ 
    x.DoSomething(); 
    x.DoMoreThings(); 
} 

class MyObject : IDisposable 
{ 
    public void Dispose() 
    { 
     if (ExceptionIsBeingHandled) 
      Rollback(); 
     else 
      Commit(); 
    } 
} 

Esto se ve casi como System.Transactions.TransactionScope, excepto que el éxito/fracaso no está determinada por una llamada a x.Complete(), sino más bien en función de si el cuerpo using se terminó normalmente.

+2

Creo que debe preguntar por qué está tratando de hacer esto. El patrón Dispose() no tiene la intención de implementar lógica de control al generar excepciones. –

+1

Siempre es un buen momento para cuestionar toda la idea. Aprecio que no es así como se debe usar "usar". Aprecio que puede conducir a un código incorrecto. Todavía estoy interesado en una respuesta :) –

+1

¿Por qué esta pregunta es downvoted? – Olli

Respuesta

11

http://www.codewrecks.com/blog/index.php/2008/07/25/detecting-if-finally-block-is-executing-for-an-manhandled-exception/ describe un "truco" para detectar si su código se ejecuta en modo de manejo de excepciones o no. Utiliza Marshal.GetExceptionPointers para ver si una excepción es "activa".

Pero hay que tener en cuenta:

Observaciones

GetExceptionPointers se expone para el apoyo del compilador de control estructurado de excepciones (SEH) solamente. NotaNota:

Este método usa SecurityAction.LinkDemand para evitar que se llame desde un código que no es de confianza; solo la persona que llama de inmediato debe tener el permiso SecurityPermissionAttribute.UnmanagedCode. Si su código puede ser llamado desde un código parcialmente confiable, no pase la entrada del usuario a los métodos de clase Marshal sin validación. Para conocer las limitaciones importantes sobre el uso del miembro LinkDemand, consulte Demanda contra LinkDemand.

+5

Esta es probablemente la única respuesta real a su pregunta, pero parece una idea seriamente mala de seguir. –

+0

Gracias, punto de mira. @ Héroe de programación: posiblemente. No puedo negar que parece ser así. Lo probaremos en un pequeño proyecto de prueba y veremos si hay problemas importantes con este enfoque. –

+2

Si sigues este camino, primero mira el siguiente blog: http://geekswithblogs.net/akraus1/archive/2008/04/08/121121.aspx – Joe

2

Una declaración de uso es simplemente azúcar sintáctica para un bloque try try. Se puede obtener lo que desea escribiendo el intento finalmente a cabo en su totalidad y luego añadir una instrucción catch para manejar su caso especial:

try 
{ 
    IDisposable x = new MyThing(); 
} 
catch (Exception exception) // Use a more specific exception if possible. 
{ 
    x.ErrorOccurred = true; // You could even pass a reference to the exception if you wish. 
    throw; 
} 
finally 
{ 
    x.Dispose(); 
} 

Dentro MYTHing puede hacerlo si lo desea, por ejemplo:

class MyThing : IDisposable 
{ 
    public bool ErrorOccurred() { get; set; } 

    public void Dispose() 
    { 
     if (ErrorOccurred) { 
      RollBack(); 
     } else { 
      Commit(); 
     } 
    } 
} 

Nota: También me pregunto por qué quieres hacer esto. Tiene algún olor a código. El método Dispose está destinado a limpiar recursos no administrados, no a manejar excepciones. Probablemente sea mejor que escriba el código de manejo de excepciones en el bloque de captura, no en el desecho, y si necesita compartir el código, realice algunas funciones de ayuda útiles a las que puede llamar desde ambos lugares.

Aquí hay una manera mejor de hacer lo que quiere:

using (IDisposable x = new MyThing()) 
{ 
    x.Foo(); 
    x.Bar(); 
    x.CommitChanges(); 
} 

class MyThing : IDisposable 
{ 
    public bool IsCommitted { get; private set; } 

    public void CommitChanges() 
    { 
     // Do stuff needed to commit. 
     IsCommitted = true; 
    } 

    public void Dispose() 
    { 
     if (!IsCommitted) 
      RollBack(); 
    } 
} 
+0

Ah, debería haber mencionado esto. El punto es que el código que entra en "atrapar" y "finalmente" es idéntico pero también no trivial. El punto es mover este código a otra parte. –

+0

Eso no es problema. El código de eliminación se llama en ambos casos, pero en el código de captura puede ejecutar el código * extra * antes de llamar al servicio. Por ejemplo, puede establecer algún valor booleano dentro de x, que se puede verificar cuando se invoca a Dispose. –

+0

Gracias por su esfuerzo, pero si todavía tenemos que colocar una cláusula try/catch "normal", entonces no tiene ningún sentido, ya que, como observa con razón, lo que viene en 'if (ErrorOcurrido)/else' debería simplemente entre al 'catch' /' finally'. La idea es ahorrar repetitivo try/catch/finally con un 'using', ya que reduciría el código repetitivo en todo el lugar. –

4

Esta información no está disponible para usted.

Usaría un patrón similar al utilizado por la clase DbTransaction: es decir, su clase IDisposable debería implementar un método similar a DbTransaction.Commit(). Su método Dispose puede entonces realizar una lógica diferente dependiendo de si se invocó o no a Commit (en el caso de DbTransaction, la transacción se retrotraerá si no se cometió explícitamente).

Los usuarios de la clase usarían entonces el siguiente patrón, similar a una típica DbTransaction:

using(MyDisposableClass instance = ...) 
{ 
    ... do whatever ... 

    instance.Commit(); 
} // Dispose logic depends on whether or not Commit was called. 

EDITAR veo que haya editado su pregunta para mostrar que está consciente de este patrón (su ejemplo usa TransactionScope). Sin embargo, creo que es la única solución realista.

2

Esto no parece ser una mala idea; simplemente no parece ser ideal en C# /. NET.

En C++ hay una función que permite que el código detecte si se está llamando debido a una excepción. Esto es de la mayor importancia en destructores RAII; es una cuestión trivial para el destructor elegir cometer o abortar dependiendo de si el flujo de control es normal o excepcional. Creo que este es un enfoque bastante natural de tomar, pero la falta de soporte integrado (y la naturaleza moralmente dudosa de la solución alternativa, se siente bastante dependiente de la implementación para mí) probablemente significa que se debe tomar un enfoque más convencional.

7

No fue una respuesta a la pregunta, sino solo una nota de que nunca terminé usando el código "aceptado" en código real, por lo que aún no se ha probado "en la naturaleza". En vez fuimos para algo como esto:

DoThings(x => 
{ 
    x.DoSomething(); 
    x.DoMoreThings(); 
}); 

donde

public void DoThings(Action<MyObject> action) 
{ 
    bool success = false; 
    try 
    { 
     action(new MyObject()); 
     Commit(); 
     success = true; 
    } 
    finally 
    { 
     if (!success) 
      Rollback(); 
    } 
} 

El punto clave es que es tan compacto como el "uso" ejemplo en la pregunta, y no utiliza ningún hacks.

Entre los inconvenientes se encuentra una penalización de rendimiento (completamente despreciable en nuestro caso), y F10 entrando en DoThings cuando realmente quiero que vaya directamente al x.DoSomething(). Ambos muy menores.

+3

Esto es mucho menos atractivo en VS2010 debido a errores de IntelliSense que tienen que ver con métodos anónimos ... https://connect.microsoft.com/VisualStudio/feedback/details/557224/intellisense-completely-fails-for-collection-inside- método anónimo? wa = wsignin1.0 –

Cuestiones relacionadas