2008-09-11 13 views
219

Llamo, mediante reflexión, un método que puede causar una excepción. ¿Cómo puedo pasar la excepción a la persona que llama sin que el reflejo de la envoltura lo coloque? Estoy volviendo a lanzar la InnerException, pero esto destruye el seguimiento de la pila. Código de ejemplo:En C#, ¿cómo puedo volver a lanzar InnerException sin perder el seguimiento de pila?

public void test1() 
    { 
     // Throw an exception for testing purposes 
     throw new ArgumentException("test1"); 
    } 

    void test2() 
    { 
     try 
     { 
      MethodInfo mi = typeof(Program).GetMethod("test1"); 
      mi.Invoke(this, null); 
     } 
     catch (TargetInvocationException tiex) 
     { 
      // Throw the new exception 
      throw tiex.InnerException; 
     } 
    } 
+1

Hay otra manera de hacer esto que no requiere vudú. Eche un vistazo a la respuesta aquí: http://stackoverflow.com/questions/15668334/preserving-exceptions-from-dynamically-invoked-methods –

+0

La excepción lanzada en el método llamado dinámicamente es la excepción interna de "Exception has sido lanzado por el objetivo de una invocación "excepción". Tiene su propio rastro de pila. Realmente no hay mucho más por lo que preocuparse. – ajeh

Respuesta

334

En .NET 4.5 existe ahora la clase ExceptionDispatchInfo.

Esto le permite capturar una excepción y volver a lo lanza sin cambiar la pila-trace:

try 
{ 
    task.Wait(); 
} 
catch(AggregateException ex) 
{ 
    ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); 
} 

Esto funciona en cualquier excepción, no sólo AggregateException.

Se introdujo debido a la característica de lenguaje C# await, que desenvuelve las excepciones internas de las instancias AggregateException para hacer que las características asíncronas del lenguaje sean más parecidas a las características del lenguaje síncrono.

+6

¿Buen candidato para un método de extensión Exception.Rethrow()? – nmarler

+8

Tenga en cuenta que la clase ExceptionDispatchInfo está en el espacio de nombres System.Runtime.ExceptionServices y no está disponible antes de .NET 4.5. – yoyo

+28

Es posible que necesite colocar un 'throw;' regular después de la línea .Throw(), porque el compilador no sabrá que .Throw() siempre arroja una excepción. 'throw;' nunca se llamará como resultado, pero al menos el compilador no se quejará si su método requiere un objeto devuelto o si es una función asíncrona. – Todd

31

creo que la mejor opción sería la de simplemente poner esto en su bloque catch:

throw; 

y luego extraer el InnerException más tarde.

+14

O elimine el try/catch por completo. –

+3

@Earwicker. La eliminación de try/catch no es una buena solución en general ya que ignora los casos en los que se requiere un código de limpieza antes de propagar la excepción en la pila de llamadas. – Jordan

+8

@Jordan - El código de limpieza debe estar en un bloque final, no en un bloque catch – Paolo

11

aún más la reflexión ...

catch (TargetInvocationException tiex) 
{ 
    // Get the _remoteStackTraceString of the Exception class 
    FieldInfo remoteStackTraceString = typeof(Exception) 
     .GetField("_remoteStackTraceString", 
      BindingFlags.Instance | BindingFlags.NonPublic); // MS.Net 

    if (remoteStackTraceString == null) 
     remoteStackTraceString = typeof(Exception) 
     .GetField("remote_stack_trace", 
      BindingFlags.Instance | BindingFlags.NonPublic); // Mono 

    // Set the InnerException._remoteStackTraceString 
    // to the current InnerException.StackTrace 
    remoteStackTraceString.SetValue(tiex.InnerException, 
     tiex.InnerException.StackTrace + Environment.NewLine); 

    // Throw the new exception 
    throw tiex.InnerException; 
} 

Tenga en cuenta que esto puede romper en cualquier momento, como campos privados no son parte de la API. Ver más discusión en Mono bugzilla.

+27

Esta es una idea muy, muy mala, ya que depende de los detalles internos no documentados sobre las clases de framework. –

+0

@Earwicker: +1 esta es una idea realmente mala –

+1

Resulta que es posible conservar el trazado de la pila sin Reflection, ver abajo. –

10

Primero: no pierda la TargetInvocationException - es información valiosa cuando querrá depurar cosas.
Segundo: Envuelva el TIE como InnerException en su propio tipo de excepción y ponga una propiedad OriginalException que vincule con lo que necesita (y mantenga todo el conjunto de llamadas intacto).
Tercero: Deja que la burbuja TIE salga de tu método.

11
public static class ExceptionHelper 
{ 
    private static Action<Exception> _preserveInternalException; 

    static ExceptionHelper() 
    { 
     MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace", BindingFlags.Instance | BindingFlags.NonPublic); 
     _preserveInternalException = (Action<Exception>)Delegate.CreateDelegate(typeof(Action<Exception>), preserveStackTrace);    
    } 

    public static void PreserveStackTrace(this Exception ex) 
    { 
     _preserveInternalException(ex); 
    } 
} 

Llame al método de extensión en su excepción antes de tirarlo, se conservará el rastro original de la pila.

+0

Tenga en cuenta que en .Net 4.0, InternalPreserveStackTrace ahora es un aspecto sin operaciones en Reflector y verá que el método está completamente vacío. –

+0

Resalte que: Estaba mirando el RC: en la versión beta, ¡han vuelto a poner la implementación! –

+3

sugerencia: cambie PreserveStackTrace para devolver ex - para lanzar una excepción, puede decir: throw ex.PreserveStackTrace(); –

5

Chicos, sois geniales ... Pronto seré nigromante.

public void test1() 
    { 
     // Throw an exception for testing purposes 
     throw new ArgumentException("test1"); 
    } 

    void test2() 
    { 
      MethodInfo mi = typeof(Program).GetMethod("test1"); 
      ((Action)Delegate.CreateDelegate(typeof(Action), mi))(); 

    } 
+1

Buena idea, pero no siempre controlas el código que llama a '.Invoke()'. –

+1

Y tampoco siempre conoce los tipos de argumentos/resultados en tiempo de compilación. –

84

Se es posible preservar el seguimiento de la pila antes de Regeneración de sin reflexión:

static void PreserveStackTrace (Exception e) 
{ 
    var ctx = new StreamingContext (StreamingContextStates.CrossAppDomain) ; 
    var mgr = new ObjectManager  (null, ctx) ; 
    var si = new SerializationInfo (e.GetType(), new FormatterConverter()) ; 

    e.GetObjectData (si, ctx) ; 
    mgr.RegisterObject (e, 1, si) ; // prepare for SetObjectData 
    mgr.DoFixups  ()   ; // ObjectManager calls SetObjectData 

    // voila, e is unmodified save for _remoteStackTraceString 
} 

Esto desperdicia una gran cantidad de ciclos en comparación con llamar InternalPreserveStackTrace a través de delegado en caché, pero tiene la ventaja de depender únicamente en la funcionalidad pública. Aquí hay un par de patrones de uso comunes para las funciones de pila-trace preservando:

// usage (A): cross-thread invoke, messaging, custom task schedulers etc. 
catch (Exception e) 
{ 
    PreserveStackTrace (e) ; 

    // store exception to be re-thrown later, 
    // possibly in a different thread 
    operationResult.Exception = e ; 
} 

// usage (B): after calling MethodInfo.Invoke() and the like 
catch (TargetInvocationException tiex) 
{ 
    PreserveStackTrace (tiex.InnerException) ; 

    // unwrap TargetInvocationException, so that typed catch clauses 
    // in library/3rd-party code can work correctly; 
    // new stack trace is appended to existing one 
    throw tiex.InnerException ; 
} 
+0

Parece genial, ¿qué debe suceder después de ejecutar estas funciones? – vdboor

+0

@vdboor: No entiendo muy bien tu pregunta. ¿La edición clarificó las cosas? –

+2

En realidad, no es mucho más lento que invocar 'InternalPreserveStackTrace' (aproximadamente un 6% más lento con 10000 iteraciones). Acceder a los campos directamente por reflexión es aproximadamente 2,5% más rápido que invocar 'InternalPreserveStackTrace' –

3

Anpother código de ejemplo que utiliza la excepción de serialización/deserialización. No requiere que el tipo de excepción real sea serializable. También usa solo métodos públicos/protegidos.

static void PreserveStackTrace(Exception e) 
    { 
     var ctx = new StreamingContext(StreamingContextStates.CrossAppDomain); 
     var si = new SerializationInfo(typeof(Exception), new FormatterConverter()); 
     var ctor = typeof(Exception).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeof(SerializationInfo), typeof(StreamingContext) }, null); 

     e.GetObjectData(si, ctx); 
     ctor.Invoke(e, new object[] { si, ctx }); 
    } 
4

Nadie ha explicado la diferencia entre ExceptionDispatchInfo.Capture(ex).Throw() y throw, así que aquí está.

La forma completa de volver a lanzar una excepción atrapada es usar ExceptionDispatchInfo.Capture(ex).Throw() (solo disponible desde .Net 4.5).

A continuación están los casos necesarios para probar esto:

1.

void CallingMethod() 
{ 
    //try 
    { 
     throw new Exception("TEST"); 
    } 
    //catch 
    { 
    // throw; 
    } 
} 

2.

void CallingMethod() 
{ 
    try 
    { 
     throw new Exception("TEST"); 
    } 
    catch(Exception ex) 
    { 
     ExceptionDispatchInfo.Capture(ex).Throw(); 
     throw; // So the compiler doesn't complain about methods which don't either return or throw. 
    } 
} 

3.

void CallingMethod() 
{ 
    try 
    { 
     throw new Exception("TEST"); 
    } 
    catch 
    { 
     throw; 
    } 
} 

4.

void CallingMethod() 
{ 
    try 
    { 
     throw new Exception("TEST"); 
    } 
    catch(Exception ex) 
    { 
     throw new Exception("RETHROW", ex); 
    } 
} 

caso 1 y el caso 2 le dará un seguimiento de pila, donde el número de línea de código fuente para el método CallingMethod es el número de línea de la línea throw new Exception("TEST").

Sin embargo, el caso 3 le dará un seguimiento de pila donde el número de línea del código fuente para el método CallingMethod es el número de línea de la llamada throw. Esto significa que si la línea throw new Exception("TEST") está rodeada por otras operaciones, no tiene idea de en qué número de línea se lanzó realmente la excepción.

El caso 4 es similar con el caso 2 porque se conserva el número de línea de la excepción original, pero no es un verdadero revés porque cambia el tipo de la excepción original.

+1

Siempre pensé que 'throw' no restablecía stacktrace (en lugar de 'throw e'). –

Cuestiones relacionadas