如果未附加调试器,则捕获异常

时间:2014-02-17 08:58:17

标签: c# vb.net visual-studio debugging

期望的行为(问题)

在C#应用程序中,我想要的是:

未附加调试器时: -

  1. 抛出异常。
  2. 异常会在堆栈中被捕获。
  3. 记录错误并继续。
  4. 附加调试器时: -

    1. 抛出异常。
    2. 调试器在抛出异常的位置中断。
    3. 通过一个例子来说明这里是如何使用条件捕获(我知道C#不支持这种方法):

      注意:当我显示我的代码抛出异常的示例时,它可能会被第三方库抛出。

      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();
          }
      }
      

      背景

      这不是一个大问题,因为有堆栈跟踪和日志记录将信息拼凑在一起,但我想知道是否有一种很好的方法来实现这一点,因为它偶尔出现(并且是其中之一)千万次削减,我不介意做没有)。另外,我从未找到过理想的方法,所以如果有的话,我感兴趣。

      在有许多步骤(例如运行测试)的程序中尤为重要。在正常的独立操作期间,如果这些步骤中的任何一个引发异常,则应记录错误并执行应转移到下一步。但是,在调试器中运行时,调试器应该在引发异常的位置中断。这将加快调试过程,因为您不需要查询堆栈跟踪,并且将保留局部变量的状态。

      这个问题的其余部分描述了我已经尝试过的事情,以便在答案中不再重复......

      已经考虑过的方法

      VB DLL中的条件捕获

      我知道C#不支持这种功能,但VB.NET支持它。所以,我可以通过在VB.NET库中实现以下内容来获得所需的行为(不要太担心代码,它基本上将方法包装在try...catch中并调用错误处理程序是一个例外,调试器没有附加):

      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
      

      请注意,Run需要有不同的重载,具体取决于参数的数量(在这种情况下,我恰好使用了4个args)。此外,对于需要在被调用方法和错误处理程序之间维护某些状态的情况,还有一个Context参数。

      然后我的代码看起来像这样:

      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 );
      }
      

      这种方法的缺点是: -

      • 项目中另一个VB.NET DLL的复杂性。
      • 不像简单try...catch那样直观 - 其他第一次来代码的人需要深入了解究竟发生了什么。

      重新掷

      代码(注意throw):

      示例:

          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;
                  }
              }
          }            
      

      问题在于,虽然throw保留了堆栈跟踪,但调试器会在发生抛出的行而不是原始抛出处中断。完全可以理解它是以这种方式发生的,但它并不是我想要发生的事情。这意味着我需要查看堆栈跟踪的异常,然后找到正确的代码行。此外,发生异常的局部变量的状态也会丢失。

      包装方法

      基本上,只需将try...catch包装在一个单独的方法中:

          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();                
              }            
          }        
      

      但是,这有很多问题:

      • 更多代码。
      • 对于更复杂的情况,事情很快就会变得难以处理,例如当有更多参数或try...catch本地存在的变量需要传递到&#39; DoSomething&#39;参考是否有子步骤。

      条件编译符号

      这可能是我最不喜欢的选择。在这种情况下,使用了一个条件编译符号,例如DEBUGGING(注意DEBUG不起作用,因为我可能在没有附加编译器的情况下运行DEBUG):

         #if !DEBUGGING           
              try
         #endif
              {
                  DoSomething();
              }
         #if !DEBUGGING           
              catch( Exception exception )
              {
                  Console.WriteLine( exception.Message ); //Do some processing on the exception
              }
         #endif
          }          
      

      问题是: -

      • 管理起来有点令人头疼,我总是在需要时不会设置它。具体来说,除了我手动设置符号定义之外,符号和附加调试器的事实之外没有任何关联。
      • #DEBUGGING使代码混乱并使try...catch的可读性降低。

      其他

      • Visual Studio设置。我还调查了不同的Visual Studio异常中断设置,但我想为代码的特定部分打开行为,而不是针对特定的异常。此外,这应该适用于各种安装。
      • 装配IL。我已经将内联IL视为生成条件异常的选项,但这需要使用第三方工具进行后构建步骤。
      • 我不认为全局(应用程序)异常处理程序会这样做,因为异常需要在应用程序堆栈中被捕获并记录下来。

      更新 - DebuggerStepThrough和Re-throw

      Steven Liekens&#39;评论表明什么似乎是一个很好的解决方案 - DebuggerStepThroughAttribute。当在包含重新抛出的方法上设置此属性时,调试器会在异常的原始位置中断,而不是在重新抛出的位置,如下所示:

      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();
          }
      }
      

      唯一的缺点是,如果您确实想要进入标记为DebuggerStepThrough的代码,或者此代码中存在异常。虽然,这是一个小缺点,因为您通常可以保持这个代码最小。

      请注意Debugger.IsAttached的使用,因为我认为它的影响很小,奇怪的heisenbugs的可能性很小,但要注意Guillaume在评论中指出使用它,并使用其他选项,如配置设置在适当的时候。

      除非有更好的方式或有人对此提出疑虑,否则我会继续这样做。

3 个答案:

答案 0 :(得分:7)

异常过滤器(C#6 +)

如果你正在使用C#6,这很容易用新的异常过滤器语法:

try
{
    DoSomething()
}
catch (Exception e) when (!System.Diagnostics.Debugger.IsAttached)
{
    Console.WriteLine(exception.Message);
}

答案 1 :(得分:5)

DebuggerStepThrough和Re-throw(接受的答案)

正如注释中所指出的,当在包含重新抛出的方法上设置DebuggerStepThroughAttribute时,调试器会在异常的原始位置中断,而不是在重新抛出的位置,如下所示:

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();
    }
}

LINQ启发的替代

我花了一些时间编写一个LINQ启发的try...catch包装器,确实实际上支持条件捕获块。

使用示例

在深入研究代码之前,这是一个基于原始要求的用法示例:

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();

这可以通过在执行When()语句中的任何内容之前首先评估Catch()语句中指定的谓词来实现。

如果您运行该示例,您会注意到调试器因导致[DebuggerStepThrough]属性的巧妙放置而导致异常的行中断。

源代码

/// <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; }
}

最终笔记

这是对我原帖的巨大修改。查看我的初始解决方案的更改历史记录。

答案 2 :(得分:0)

您可以包装异常并捕获特定类型的异常,这样在调试时,异常没有定义的catch行为,调试器将在抛出代码中中断。

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)
    {
    }
}