c# - Stack de pila incorrecto por re-lanzamiento
stack-trace throw (12)
¿Cómo puedo preservar la stack stack real?
Lanza una nueva excepción e incluye la excepción original como la excepción interna.
pero eso es feo ... más tiempo ... te hace elegir la excepción perfecta para tirar ...
Estás equivocado sobre lo feo, pero a la derecha sobre los otros dos puntos. La regla general es: no atrapar a menos que vayas a hacer algo con él, como envolverlo, modificarlo, tragarlo o iniciar sesión. Si decides catch
y luego throw
nuevo, asegúrate de estar haciendo algo con él, de lo contrario, simplemente déjalo burbujear.
También puede estar tentado de poner un catch simplemente para que pueda realizar un breakpoint dentro del catch, pero el depurador de Visual Studio tiene suficientes opciones para hacer innecesaria esa práctica, intente utilizar excepciones de primera oportunidad o puntos de interrupción condicionales en su lugar.
Vuelvo a lanzar una excepción con "throw;" pero la stacktrace es incorrecta:
static void Main(string[] args) {
try {
try {
throw new Exception("Test"); //Line 12
}
catch (Exception ex) {
throw; //Line 15
}
}
catch (Exception ex) {
System.Diagnostics.Debug.Write(ex.ToString());
}
Console.ReadKey();
}
El stacktrace correcto debería ser:
System.Exception: Test at ConsoleApplication1.Program.Main(String[] args) in Program.cs:Line 12
Pero entiendo:
System.Exception: Test at ConsoleApplication1.Program.Main(String[] args) in Program.cs:Line 15
Pero la línea 15 es la posición del "tiro". He probado esto con .NET 3.5.
¿Quieres tu número de línea derecho? Solo use una prueba / captura por método. En sistemas, bueno ... solo en la capa UI, no en lógica o acceso a datos, esto es muy molesto, porque si necesita transacciones de bases de datos, bueno, no deberían estar en la capa UI, y no tendrá el número de línea correcto, pero si no los necesita, no vuelva a lanzar con ni sin excepción en captura ...
Código de muestra de 5 minutos:
Archivo de menú -> Nuevo proyecto , coloque tres botones y llame al siguiente código en cada uno:
private void button1_Click(object sender, EventArgs e)
{
try
{
Class1.testWithoutTC();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine + Environment.NewLine + "In. Ex.: " + ex.InnerException);
}
}
private void button2_Click(object sender, EventArgs e)
{
try
{
Class1.testWithTC1();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine + Environment.NewLine + "In. Ex.: " + ex.InnerException);
}
}
private void button3_Click(object sender, EventArgs e)
{
try
{
Class1.testWithTC2();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine + Environment.NewLine + "In. Ex.: " + ex.InnerException);
}
}
Ahora, crea una nueva Clase:
class Class1
{
public int a;
public static void testWithoutTC()
{
Class1 obj = null;
obj.a = 1;
}
public static void testWithTC1()
{
try
{
Class1 obj = null;
obj.a = 1;
}
catch
{
throw;
}
}
public static void testWithTC2()
{
try
{
Class1 obj = null;
obj.a = 1;
}
catch (Exception ex)
{
throw ex;
}
}
}
Corre ... ¡el primer botón es hermoso!
Creo que esto es menos un caso de cambio de traza de pila y más que ver con la forma en que se determina el número de línea para el seguimiento de la pila. Probándolo en Visual Studio 2010, el comportamiento es similar al que esperaría de la documentación de MSDN: "throw ex"; reconstruye la traza de la pila desde el punto de esta declaración, "arrojar"; deja el rastro de la pila como tal, excepto que dondequiera que se vuelva a lanzar la excepción, el número de línea es la ubicación del nuevo lanzamiento y no la llamada a la que llegó la excepción.
Entonces con "lanzar"; el árbol de llamadas al método no se modifica, pero los números de línea pueden cambiar.
Me he encontrado con esto algunas veces, y puede ser por diseño y simplemente no está completamente documentado. Puedo entender por qué pudieron haber hecho esto, ya que la ubicación del nuevo lanzamiento es muy útil para saberlo, y si tus métodos son lo suficientemente simples, la fuente original generalmente sería obvia de todos modos.
Como muchas otras personas han dicho, generalmente es mejor no captar la excepción a menos que realmente tenga que hacerlo, y / o lo va a tratar en ese momento.
Nota interesante: Visual Studio 2010 ni siquiera me deja construir el código tal como se presenta en la pregunta, ya que recoge el error de dividir por cero en el momento de la compilación.
Es algo que puede considerarse como se espera. La modificación del seguimiento de pila es el caso habitual si especifica throw ex;
, FxCop lo notificará que la pila está modificada. En caso de que hagas un throw;
, no se genera ninguna advertencia, pero aún así, la traza se modificará. Desafortunadamente, por ahora, es mejor no atrapar al ex o lanzarlo como uno interno. Creo que debería considerarse como un impacto de Windows o algo así , editado . Jeff Richter describe esta situación con más detalle en su "CLR vía C #" :
El siguiente código arroja el mismo objeto de excepción que capturó y hace que el CLR restablezca su punto de inicio para la excepción:
private void SomeMethod() { try { ... } catch (Exception e) { ... throw e; // CLR thinks this is where exception originated. // FxCop reports this as an error } }
Por el contrario, si vuelve a lanzar un objeto de excepción utilizando la palabra clave throw por sí mismo, el CLR no restablece el punto de partida de la pila. El siguiente código vuelve a arrojar el mismo objeto de excepción que capturó, lo que hace que CLR no restablezca su punto de inicio para la excepción:
private void SomeMethod() { try { ... } catch (Exception e) { ... throw; // This has no effect on where the CLR thinks the exception // originated. FxCop does NOT report this as an error } }
De hecho, la única diferencia entre estos dos fragmentos de código es lo que el CLR cree que es la ubicación original donde se lanzó la excepción. Desafortunadamente, cuando lanza o vuelve a lanzar una excepción, Windows restablece el punto de partida de la pila. Entonces, si la excepción no se controla, la ubicación de la pila que se informa a Windows Error Reporting es la ubicación del último lanzamiento o reenvío, aunque el CLR conoce la ubicación de la pila donde se lanzó la excepción original. Esto es desafortunado porque hace que las aplicaciones de depuración que han fallado en el campo sean mucho más difíciles. Algunos desarrolladores han encontrado esto tan intolerable que han elegido una forma diferente de implementar su código para garantizar que el seguimiento de la pila realmente refleje la ubicación donde se lanzó originalmente una excepción:
private void SomeMethod() { Boolean trySucceeds = false; try { ... trySucceeds = true; } finally { if (!trySucceeds) { /* catch code goes in here */ } } }
Esta es una limitación bien conocida en la versión de Windows del CLR. Utiliza el soporte integrado de Windows para manejo de excepciones (SEH). El problema es que está basado en un marco de pila y un método tiene solo un marco de pila. Puede resolver el problema fácilmente moviendo el bloque try / catch interno a otro método auxiliar, creando así otro marco de pila. Otra consecuencia de esta limitación es que el compilador JIT no alineará ningún método que contenga una declaración try.
Esto se debe a que atrapó la Exception
de la Línea 12 y la volvió a colocar en la Línea 15 , por lo que el Rastreo de pila lo toma como efectivo, que la Exception
fue lanzada desde allí.
Para gestionar mejor las excepciones, simplemente debe usar try...finally
, y dejar que la Exception
no controlada se Exception
.
Hay una pregunta duplicada aquí .
Como lo entiendo, tirar; se compila en la instrucción MSIL ''rethrow'' y modifica el último cuadro de la pila-rastreo.
Esperaría que mantuviera el stack-trace original y agregue la línea donde fue relanzado, pero aparentemente solo puede haber un frame de pila por llamada de método .
Conclusión: evitar usar throw; y envuelva su excepción en una nueva en volver a tirar, no es feo, es la mejor práctica.
Lanzar dos veces en el mismo método es probablemente un caso especial: no he podido crear un seguimiento de pila donde se siguen líneas diferentes en el mismo método. Como dice la palabra, un "rastro de pila" le muestra los marcos de pila que atravesó una excepción. ¡Y solo hay un marco de pila por llamada de método!
Si arrojas desde otro método, throw;
no eliminará la entrada de Foo()
, como se esperaba:
static void Main(string[] args)
{
try
{
Rethrower();
}
catch (Exception ex)
{
Console.Write(ex.ToString());
}
Console.ReadKey();
}
static void Rethrower()
{
try
{
Foo();
}
catch (Exception ex)
{
throw;
}
}
static void Foo()
{
throw new Exception("Test");
}
Si modifica Rethrower()
y reemplaza throw;
por throw ex;
, la entrada Foo()
en el seguimiento de pila desaparece. De nuevo, ese es el comportamiento esperado.
No estoy seguro de si esto es por diseño, pero creo que siempre ha sido así.
Si el lanzamiento original nuevo Excepción se realiza en un método diferente, el resultado del lanzamiento debe tener el nombre y el número de línea del método original y luego el número de línea en el principal donde se vuelve a lanzar la excepción.
Si usa throw ex, entonces el resultado será la línea en main donde la excepción es rethrow.
En otras palabras, throw ex pierde toda la stacktrace, mientras que throw conserva el historial de seguimiento de pila (es decir, detalles de los métodos de nivel inferior). Pero si su excepción se genera por el mismo método que su nuevo lanzamiento, puede perder cierta información.
NÓTESE BIEN. Si escribe un programa de prueba muy simple y pequeño, el Marco a veces puede optimizar cosas y cambiar un método para que sea código en línea, lo que significa que los resultados pueden diferir de un programa "real".
OK, parece que hay un error en .NET Framework, si lanza una excepción y la vuelve a lanzar en el mismo método, se pierde el número de línea original (será la última línea del método).
Afortunadamente, un tipo inteligente llamado Fabrice MARGUERIE encontró weblogs.asp.net/fmarguerie/archive/2008/01/02/… a este error. A continuación está mi versión, que puedes probar en este .NET Fiddle .
private static void RethrowExceptionButPreserveStackTrace(Exception exception)
{
System.Reflection.MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace",
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
preserveStackTrace.Invoke(exception, null);
throw exception;
}
Ahora tome la excepción como siempre, pero en lugar de tirar; solo llame a este método, y listo, ¡se conservará el número de línea original!
Puede conservar la traza de pila usando
ExceptionDispatchInfo.Capture(ex);
Aquí hay una muestra de código:
static void CallAndThrow()
{
throw new ApplicationException("Test app ex", new Exception("Test inner ex"));
}
static void Main(string[] args)
{
try
{
try
{
try
{
CallAndThrow();
}
catch (Exception ex)
{
var dispatchException = ExceptionDispatchInfo.Capture(ex);
// rollback tran, etc
dispatchException.Throw();
}
}
catch (Exception ex)
{
var dispatchException = ExceptionDispatchInfo.Capture(ex);
// other rollbacks
dispatchException.Throw();
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine(ex.InnerException.Message);
Console.WriteLine(ex.StackTrace);
}
Console.ReadLine();
}
El resultado será algo así como:
Test app ex Test inner ex at TestApp.Program.CallAndThrow() in D:/Projects/TestApp/TestApp/Program.cs:line 19 at TestApp.Program.Main(String[] args) in D:/Projects/TestApp/TestApp/Program.cs:line 30 --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at TestApp.Program.Main(String[] args) in D:/Projects/TestApp/TestApp/Program.cs:line 38 --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at TestApp.Program.Main(String[] args) in D:/Projects/TestApp/TestApp/Program.cs:line 47
Editar / Reemplazar
El comportamiento es realmente diferente, pero sutilmente. En cuanto a por qué el comportamiento es diferente, tendré que ceder ante un experto de CLR.
EDITAR: la respuesta de AlexD parece indicar que esto es por diseño.
Lanzar la excepción en el mismo método que lo atrapa confunde la situación un poco, así que arrojemos una excepción de otro método:
class Program
{
static void Main(string[] args)
{
try
{
Throw();
}
catch (Exception ex)
{
throw ex;
}
}
public static void Throw()
{
int a = 0;
int b = 10 / a;
}
}
Si throw;
se usa, la pila de llamadas es (números de línea reemplazados con código):
at Throw():line (int b = 10 / a;)
at Main():line (throw;) // This has been modified
Si throw ex;
se usa, la pila de llamadas es:
at Main():line (throw ex;)
Si no se detecta una excepción, la pila de llamadas es:
at Throw():line (int b = 10 / a;)
at Main():line (Throw())
Probado en .NET 4 / VS 2010