c# - tipos - Capturar excepción si el depurador no está adjunto
tipos de excepciones en c# (3)
Comportamiento deseado (la pregunta)
En una aplicación C #, lo que me gustaría es esto:
Cuando el depurador no está conectado: -
- Se lanza una excepción.
- La excepción está atrapada más arriba en la pila.
- Error de registro y continuar
Cuando el depurador está conectado:
- Se lanza una excepción.
- El depurador se rompe en el punto donde se lanza la excepción.
Para ilustrar a modo de ejemplo, aquí es cómo podría funcionar con una captura condicional ( sé que esto no es compatible con C # ):
Nota: mientras estoy mostrando un ejemplo de la excepción lanzada por mi código, puede ser lanzado por una biblioteca de terceros.
static void DoSomething()
{
//This is where I would like the debugger to break execution and show the exception
throw new Exception( "Something went wrong!" );
}
static public void DoSomeStep()
{
try
{
DoSomething();
}
catch( Exception exception when System.Diagnostics.Debugger.IsAttached == false ) //If the debugger is attached don''t catch
{
Console.WriteLine( exception.Message ); //Do some processing on the exception
}
}
static void Main( string[] args )
{
for( int i = 0; i < 10; i++ )
{
DoSomeStep();
}
}
Fondo
Esto no es un gran problema, ya que hay rastros de pila y registro para juntar la información, pero me preguntaba si hay una buena manera de lograr esto, ya que aparece de vez en cuando (y es uno de esos miles de cortes que No me importaría prescindir) Además, nunca he encontrado un método ideal, por lo que estoy interesado si hay uno.
Es particularmente relevante en un programa donde hay una serie de pasos (como ejecutar pruebas). Durante la operación autónoma normal, si alguno de estos pasos genera una excepción, se debe registrar un error y la ejecución debe pasar al siguiente paso. Sin embargo, cuando se ejecuta en el depurador, el depurador debe romperse en el punto donde se produjo la excepción. Esto acelerará el proceso de depuración ya que no necesita consultar los rastros de pila y se conservará el estado de las variables locales.
El resto de esta pregunta describe cosas que ya he intentado para que no se repitan en las respuestas ...
Enfoques que ya han sido considerados
Captura condicional en VB DLL
Sé que esto no es compatible con C #, pero es compatible con VB.NET. Entonces, puedo obtener el comportamiento deseado implementando lo siguiente en una biblioteca de VB.NET (no se preocupe demasiado por el código, básicamente ajusta un método en un try...catch
y llama a un controlador de errores si hay un excepción y el depurador no está adjunto):
Public Module DebuggerNoCatch
Public Function Run(Of T, U, V, W, X)(func As Func(Of T, U, V, W, X, Boolean), arg1 As T, arg2 As U, arg3 As V, arg4 As W, context As X, errorHandler As Action(Of System.Exception, X)) As Boolean
Dim result As Boolean = False
Try
result = func(arg1, arg2, arg3, arg4, context)
Catch ex As Exception When Not Debugger.IsAttached
errorHandler(ex, context)
result = False
End Try
Return result
End Function
End Module
Tenga en cuenta que debe haber diferentes sobrecargas para Run
dependiendo de la cantidad de argumentos (en este caso, el mío simplemente usa 4 argumentos). Además, hay un parámetro Context
para los casos en que se necesita mantener algún estado entre el método que se llama y el controlador de errores.
Entonces mi código se ve así:
static bool DoSomething( int a, int b, int c, int d, RunContext context )
{
//Now the debugger break at this point - hooray!
throw new Exception( "Something went wrong!" );
return true;
}
static void HandleException( Exception exception, RunContext context )
{
//Only see this when not attached in the debugger
Console.WriteLine( exception.Message ); //Do some processing on the exception
}
class RunContext{ } //context information - not used in this example
static public void DoSomeStep()
{
DebuggerNoCatch.Run<int, int, int, int, RunContext>( DoSomething, 1, 1, 1, 1, new RunContext(), HandleException );
}
Los inconvenientes de este enfoque son:
- La complejidad añadida de otra DLL VB.NET en el proyecto.
- No es tan intuitivo como un simple
try...catch
: otras personas que accedan al código por primera vez tendrían que investigar para entender exactamente qué está pasando.
Volver a tirar
El código (nota el throw
):
Ejemplo:
static public void DoSomeStep()
{
try
{
DoSomething();
}
catch( Exception exception )
{
Console.WriteLine( exception.Message ); //Do some processing on the exception
//If the debugger is attached throw, otherwise just continue to the next step
if( System.Diagnostics.Debugger.IsAttached == true )
{
//This is where the debugger breaks execution and shows the exception
throw;
}
}
}
El problema con esto es que mientras throw
conserva el seguimiento de pila, el depurador se rompe en la línea donde se produce el lanzamiento en lugar del lanzamiento original. Tiene todo el sentido que sucede de esta manera, pero no es lo que quiero que suceda. Significa que necesito mirar dentro de la excepción para stacktrace y luego encontrar la línea correcta de código. Además, el estado de las variables locales donde ocurrió la excepción se pierde.
Método Wrapper
Básicamente, simplemente envuelva la try...catch
en un método diferente:
static void DoSomething()
{
//This is where I would like the debugger to break execution and show the exception
throw new Exception( "Something went wrong!" );
}
static void DoSomethingContinueOnError()
{
try
{
DoSomething();
}
catch( Exception exception )
{
Console.WriteLine( exception.Message ); //Do some processing on the exception
}
}
static public void DoSomeStep()
{
if( System.Diagnostics.Debugger.IsAttached == false )
{
DoSomethingContinueOnError();
}
else
{
DoSomething();
}
}
Pero, hay una serie de problemas con esto:
- Más código.
- Las cosas se vuelven rápidamente difíciles de manejar para casos más complicados, como cuando hay más parámetros o hay variables locales para
try...catch
que se establecen por necesidad se pasan a ''DoSomething'' por referencia de si hay subpasos.
Símbolos de compilación condicional
Esta es probablemente mi opción menos favorita. En este caso, se usa un símbolo de compilación condicional como DEBUGGING (note que DEBUG no funcionará porque puedo estar ejecutando DEBUG sin el compilador adjunto):
#if !DEBUGGING
try
#endif
{
DoSomething();
}
#if !DEBUGGING
catch( Exception exception )
{
Console.WriteLine( exception.Message ); //Do some processing on the exception
}
#endif
}
Los problemas son:
- Esto es todo un dolor de cabeza para manejar y, invariablemente, no lo tengo configurado cuando lo necesito. Específicamente, el símbolo y el hecho de que el depurador esté conectado no están relacionados de ninguna otra manera que la configuración manual de la definición del símbolo.
-
#DEBUGGING
el código y hace que eltry...catch
less legible.
Otro
- Configuración de Visual Studio. También investigué las diferentes configuraciones de interrupción de excepción de Visual Studio, pero deseo activar el comportamiento para partes específicas del código, no para excepciones específicas. Además, esto debería funcionar en todas las instalaciones.
- Asamblea IL. He considerado IL en línea como una opción para generar la excepción condicional, pero esto requiere pasos posteriores a la construcción con herramientas de terceros.
- No creo que los manejadores de excepciones globales (aplicaciones) lo hagan porque la excepción debe capturarse y registrarse más abajo en la pila de la aplicación.
Actualización - DebuggerStepThrough and Re-throw
El comentario de Steven Liekens indica lo que parece ser una buena solución: DebuggerStepThroughAttribute
. Cuando este atributo se establece en un método que contiene un re-lanzamiento, el depurador se rompe en el punto original de la excepción, no donde se vuelve a lanzar como se muestra a continuación:
static bool DoSomething()
{
//This is where the debugger now breaks execution
throw new Exception( "Something went wrong!" );
return true;
}
[DebuggerStepThrough]
static public void DoSomeStep()
{
try
{
DoSomething();
}
catch( Exception exception )
{
Console.WriteLine( exception.Message );
if( Debugger.IsAttached == true )
{
//the debugger no longer breaks here
throw;
}
}
}
static void Main( string[] args )
{
for( int i = 0; i < 10; i++ )
{
DoSomeStep();
}
}
El único inconveniente es si realmente desea ingresar al código marcado como DebuggerStepThrough
o si hay una excepción en este código. Sin embargo, este es un inconveniente menor porque generalmente puede mantener este código mínimo.
Tenga en cuenta el uso de Debugger.IsAttached
porque creo que su impacto aquí es mínimo y la probabilidad de heisenbugs extraños es mínima, pero tenga cuidado de usarla como lo señala Guillaume en los comentarios y use otra opción como configuración de configuración cuando corresponda.
Seguiré con esto a menos que una mejor manera o alguien genere preocupaciones al respecto.
Filtros de excepción (C # 6+)
Si está utilizando C # 6, esto es fácil de hacer con la nueva sintaxis del filtro de excepción:
try
{
DoSomething()
}
catch (Exception e) when (!System.Diagnostics.Debugger.IsAttached)
{
Console.WriteLine(exception.Message);
}
DebuggerStepThrough and Re-throw (Respuesta aceptada)
Como se señala en los comentarios, cuando DebuggerStepThroughAttribute
se establece en un método que contiene un re-throw, el depurador se rompe en el punto original de la excepción, no donde se vuelve a lanzar como se muestra a continuación:
static bool DoSomething()
{
//This is where the debugger now breaks execution
throw new Exception( "Something went wrong!" );
return true;
}
[DebuggerStepThrough]
static public void DoSomeStep()
{
try
{
DoSomething();
}
catch( Exception exception )
{
Console.WriteLine( exception.Message );
if( Debugger.IsAttached == true )
{
//the debugger no longer breaks here
throw;
}
}
}
static void Main( string[] args )
{
for( int i = 0; i < 10; i++ )
{
DoSomeStep();
}
}
Alternativa inspirada en LINQ
Pasé un tiempo escribiendo un contenedor try...catch
inspirado en LINQ que admite bloques de catch condicionales.
Ejemplo de uso
Antes de sumergirse en el código, aquí hay un ejemplo de uso basado en los requisitos originales:
DangerousOperation
.Try(() =>
{
throw new NotImplementedException();
})
.Catch((NotImplementedException exception) =>
{
Console.WriteLine(exception.Message);
}).When(ex => !Debugger.IsAttached)
.Catch((NotSupportedException exception) =>
{
Console.WriteLine("This block is ignored");
}).When(ex => !Debugger.IsAttached)
.Catch<InvalidProgramException>() /* specifying a handler is optional */
.Catch() /* In fact, specifying the exception type is also optional */
.Finally(() =>
{
Console.WriteLine("Goodbye");
}).Execute();
Esto funciona al evaluar primero el predicado especificado en la instrucción When()
antes de ejecutar lo que está en la instrucción Catch()
.
Si ejecuta el ejemplo, notará que el depurador se rompe en la línea que causa la excepción como resultado de la colocación inteligente de un atributo [DebuggerStepThrough]
.
Código fuente
/// <summary>
/// Factory. Provides a static method that initializes a new try-catch wrapper.
/// </summary>
public static class DangerousOperation
{
/// <summary>
/// Starts a new try-catch block.
/// </summary>
/// <param name="action">The ''try'' block''s action.</param>
/// <returns>Returns a new instance of the <see cref="TryCatchBlock"/> class that wraps the ''try'' block.</returns>
public static TryCatchBlock Try()
{
return new TryCatchBlock();
}
/// <summary>
/// Starts a new try-catch block.
/// </summary>
/// <param name="action">The ''try'' block''s action.</param>
/// <returns>Returns a new instance of the <see cref="TryCatchBlock"/> class that wraps the ''try'' block.</returns>
public static TryCatchBlock Try(Action action)
{
return new TryCatchBlock(action);
}
}
/// <summary>
/// Wraps a ''try'' or ''finally'' block.
/// </summary>
public class TryCatchBlock
{
private bool finalized;
/// <summary>
/// Initializes a new instance of the <see cref="TryCatchBlock"/> class;
/// </summary>
public TryCatchBlock()
{
this.First = this;
}
/// <summary>
/// Initializes a new instance of the <see cref="TryCatchBlock"/> class;
/// </summary>
/// <param name="action">The ''try'' or ''finally'' block''s action.</param>
public TryCatchBlock(Action action)
: this()
{
this.Action = action;
}
protected TryCatchBlock(TryCatchBlock antecedent)
{
if ( antecedent == null )
{
throw new ArgumentNullException("antecedent");
}
if ( antecedent.finalized )
{
throw new InvalidOperationException("This block has been finalized with a call to ''Finally()''");
}
this.First = antecedent.First;
this.Antecedent = antecedent;
antecedent.Subsequent = this;
}
protected TryCatchBlock(TryCatchBlock antecedent, Action action)
: this(antecedent)
{
this.Action = action;
}
public Action Action { get; set; }
/// <summary>
/// Gets the ''try'' block.
/// </summary>
public TryCatchBlock First { get; private set; }
/// <summary>
/// Gets the next block.
/// </summary>
public TryCatchBlock Antecedent { get; private set; }
/// <summary>
/// Gets the previous block.
/// </summary>
public TryCatchBlock Subsequent { get; private set; }
/// <summary>
/// Creates a new ''catch'' block and adds it to the chain.
/// </summary>
/// <returns>Returns a new instance of the <see cref="TryCatchBlock{TException}"/> class that wraps a ''catch'' block.</returns>
public TryCatchBlock<Exception> Catch()
{
return new TryCatchBlock<Exception>(this);
}
/// <summary>
/// Creates a new ''catch'' block and adds it to the chain.
/// </summary>
/// <returns>Returns a new instance of the <see cref="TryCatchBlock{TException}"/> class that wraps a ''catch'' block.</returns>
public TryCatchBlock<Exception> Catch(Action<Exception> action)
{
return new TryCatchBlock<Exception>(this, action);
}
/// <summary>
/// Creates a new ''catch'' block and adds it to the chain.
/// </summary>
/// <typeparam name="TException">The type of the exception that this block will catch.</typeparam>
/// <returns>Returns a new instance of the <see cref="TryCatchBlock{TException}"/> class that wraps a ''catch'' block.</returns>
public TryCatchBlock<TException> Catch<TException>() where TException : System.Exception
{
return new TryCatchBlock<TException>(this);
}
/// <summary>
/// Creates a new ''catch'' block and adds it to the chain.
/// </summary>
/// <typeparam name="TException">The type of the exception that this block will catch.</typeparam>
/// <param name="action">The ''catch'' block''s action.</param>
/// <returns>Returns a new instance of the <see cref="TryCatchBlock{TException}"/> class that wraps a ''catch'' block.</returns>
public TryCatchBlock<TException> Catch<TException>(Action<TException> action) where TException : System.Exception
{
return new TryCatchBlock<TException>(this, action);
}
/// <summary>
/// Creates a new ''finally'' block and finalizes the chain.
/// </summary>
/// <returns>Returns a new instance of the <see cref="TryCatchBlock"/> class that wraps the ''finally'' block.</returns>
public TryCatchBlock Finally()
{
return new TryCatchBlock(this) { finalized = true };
}
/// <summary>
/// Creates a new ''finally'' block and finalizes the chain.
/// </summary>
/// <param name="action">The ''finally'' block''s action.</param>
/// <returns>Returns a new instance of the <see cref="TryCatchBlock"/> class that wraps the ''finally'' block.</returns>
public TryCatchBlock Finally(Action action)
{
return new TryCatchBlock(this, action) { finalized = true };
}
/// <summary>
/// Gets a value indicating whether this ''catch'' wrapper can handle and should handle the specified exception.
/// </summary>
/// <param name="exception">The exception.</param>
/// <returns>Returns <c>true</c> if the exception can be handled; otherwise <c>false</c>.</returns>
public virtual bool CanHandle(Exception exception)
{
return false;
}
/// <summary>
/// Handles the specified exception.
/// </summary>
/// <param name="exception">The exception.</param>
public virtual void Handle(Exception exception)
{
throw new InvalidOperationException("This is not a ''catch'' block wrapper.");
}
/// <summary>
/// Executes the chain of ''try-catch'' wrappers.
/// </summary>
//[DebuggerStepThrough]
public void Execute()
{
TryCatchBlock current = this.First;
try
{
if ( current.Action != null )
{
current.Action();
}
}
catch ( Exception exception )
{
while ( current.Subsequent != null )
{
current = current.Subsequent;
if ( current.CanHandle(exception) )
{
current.Handle(exception);
break;
}
if ( current.Subsequent == null )
{
throw;
}
}
}
finally
{
while ( current.Subsequent != null )
{
current = current.Subsequent;
if ( current.finalized && current.Action != null )
{
current.Action();
}
}
}
}
}
/// <summary>
/// Wraps a ''catch'' block.
/// </summary>
/// <typeparam name="TException">The type of the exception that this block will catch.</typeparam>
public class TryCatchBlock<TException> : TryCatchBlock where TException : System.Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="TryCatchBlock{TException}"/> class;
/// </summary>
/// <param name="antecedent">The ''try'' or ''catch'' block that preceeds this ''catch'' block.</param>
public TryCatchBlock(TryCatchBlock antecedent)
: base(antecedent) { }
/// <summary>
/// Initializes a new instance of the <see cref="TryCatchBlock{TException}"/> class;
/// </summary>
/// <param name="antecedent">The ''try'' or ''catch'' block that preceeds this ''catch'' block.</param>
/// <param name="action">The ''catch'' block''s action.</param>
public TryCatchBlock(TryCatchBlock antecedent, Action<TException> action)
: base(antecedent)
{
this.Action = action;
}
/// <summary>
/// Sets a predicate that determines whether this block should handle the exception.
/// </summary>
/// <param name="predicate">The method that defines a set of criteria.</param>
/// <returns>Returns the current instance.</returns>
public TryCatchBlock<TException> When(Predicate<TException> predicate)
{
this.Predicate = predicate;
return this;
}
/// <summary>
/// Gets a value indicating whether this ''catch'' wrapper can handle and should handle the specified exception.
/// </summary>
/// <param name="exception">The exception.</param>
/// <returns>Returns <c>True</c> if the exception can be handled; otherwise false.</returns>
public override bool CanHandle(Exception exception)
{
if ( exception == null )
{
throw new ArgumentNullException("exception");
}
if ( !typeof(TException).IsAssignableFrom(exception.GetType()) )
{
return false;
}
if ( Predicate == null )
{
return true;
}
return Predicate((TException) exception);
}
/// <summary>
/// Handles the specified exception.
/// </summary>
/// <param name="exception">The exception.</param>
public override void Handle(Exception exception)
{
if ( this.Action != null )
{
this.Action((TException) exception);
}
}
/// <summary>
/// Gets the exception handler.
/// </summary>
public Action<TException> Action { get; private set; }
/// <summary>
/// Gets the predicate that determines whether this wrapper should handle the exception.
/// </summary>
public Predicate<TException> Predicate { get; private set; }
}
Notas finales
Esta es una gran edición de mi publicación original. Mira el historial de cambios para mi solución inicial.
Podría envolver la excepción y detectar un tipo específico de excepción, de esa manera, al depurar, no existe un comportamiento de captura definido para la excepción y el depurador se romperá en el código de lanzamiento.
class Program
{
static void Main(string[] args)
{
try
{
NotImplementedMethod();
}
catch (NotImplementedException)
{
Console.WriteLine("Exception caught");
}
Console.Read();
}
public static void NotImplementedMethod()
{
throw DebugException.Wrap(new NotImplementedException());//Breaks here when debugger is attached
}
}
public class DebugException : Exception
{
public static Exception Wrap(Exception innerException)
{
if(Debugger.IsAttached)
{
return new DebugException(innerException);
}
else
{
return innerException;
}
}
public DebugException(Exception innerException)
: base("Debug exception", innerException)
{
}
}