【问题标题】:Catch exception if debugger is not attached如果未附加调试器,则捕获异常
【发布时间】:2014-03-16 11:37:33
【问题描述】:

期望的行为(问题)

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

没有附加调试器时:-

  1. 抛出异常。
  2. 异常在堆栈中的较高位置被捕获。
  3. 记录错误并继续。

附加调试器时:-

  1. 抛出异常。
  2. 调试器在抛出异常的地方中断。

为了举例说明,下面是它如何与条件捕获一起工作(我知道这在 C# 中不受支持):

注意:虽然我展示了我的代码抛出异常的示例,但它可能是由第 3 方库抛出的。

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 个参数)。此外,还有一个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 的局部变量需要通过引用传递给“DoSomething”时是否有子步骤.

条件编译符号

这可能是我最不喜欢的选项。在这种情况下,使用了条件编译符号,例如 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 作为生成条件异常的选项,但这需要使用 3rd 方工具进行构建后步骤。
  • 我认为全局(应用程序)异常处理程序不会这样做,因为需要捕获异常并将其记录在应用程序堆栈的较低位置。

更新 - DebuggerStepThrough 和重新抛出

Steven Liekens 的评论表明似乎是一个很好的解决方案 - 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 在 cmets 中指出的那样,并在适当的时候使用其他选项,例如配置设置.

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

【问题讨论】:

  • 查看 Microsoft 企业库:msdn.microsoft.com/en-us/library/dn169621.aspx 您可能想要的是异常处理应用程序块或其他块之一。虽然它添加了另一个 dll(甚至更多),但您可以获得经过良好测试和使用的组件,而不是制作自己的(可能不是最佳的)解决方案。
  • @Samuel,感谢您的想法。我可以看到异常处理块对于为应用程序堆栈的每一层适当地包装和传播异常很有用,但似乎没有任何关于在调试器中保留异常位置的内容。我会犹豫是否要为我的轻量级应用程序走这条路,而不知道它会解决它。
  • 对于只存在于您的 RC 但不存在于您的 DEBUGGING 构建中的全局异常处理程序该怎么说?这样,每个未处理的异常都将在调试时抛出它发生的地方,并且每个异常都将由全局处理程序处理。当然这不会干扰所有已处理异常
  • @MrPaulch - 不确定我是否遵循 - 在调试器中不得处理已处理的异常,而是调试器应该在它们被抛出的地方中断。另外,如果我在调试器中运行 RC 来追踪问题会发生什么?
  • 看,你已经建议了#if DEBUGGING try ... catch ... etc.自己。我建议您不要在每个 try 流行语上使用该方法,而是在全局异常处理程序的定义上使用该方法。因此,仅当您编译发布版本时才会添加全局异常处理程序。在这种情况下,您不需要在您预计不会出现异常的区域周围尝试 catch 块。

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


【解决方案1】:

异常过滤器 (C# 6+)

如果您使用的是 C# 6,使用新的异常过滤器语法很容易做到这一点:

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

【讨论】:

  • 酷。很高兴听到它。
【解决方案2】:

DebuggerStepThrough 和重新抛出(接受的答案)

正如 cmets 中所指出的,当在包含重新抛出的方法上设置 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();

在执行Catch() 语句中的任何内容之前,首先评估When() 语句中指定的谓词。

如果您运行该示例,您会注意到由于巧妙地放置了 [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; }
}

最后说明

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

【讨论】:

  • 哈。好吧,我必须给你一个 +1 的创造力,这看起来可能会奏效(还没有尝试过),但我认为我的一些同事会感到惊讶。
  • 我认为这是我在一个语句中塞进的最多的语言技巧。我为自己感到骄傲。
  • @acarlon 我已经用一个严肃的解决方案更新了我的答案。
  • @StevenLiekens 使用 DangerousOperation 调用 finally 是强制性的,否则异常可能会丢失。这不是很清楚,因为它是常规异常处理的可选。
  • @Guillaume 你有什么建议?
【解决方案3】:

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

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

【讨论】:

  • 谢谢,问题是第三方代码可能会抛出异常,例如引用的库。我会在我的问题中更清楚地说明这一点。
猜你喜欢
  • 2014-06-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-14
  • 2014-05-11
  • 2019-03-13
  • 1970-01-01
  • 2011-09-02
相关资源
最近更新 更多