Исключить исключение, если отладчик не подключен

Желаемое поведение (вопрос)

В приложении С# я бы хотел:

Когда отладчик не подключен: -

  1. Исключение выбрано.
  2. Исключение происходит в стеке выше.
  3. Ошибка журнала и продолжить.

Когда отладчик подключен: -

  1. Исключение выбрано.
  2. Отладчик ломается в точке, где генерируется исключение.

Чтобы проиллюстрировать в качестве примера, вот как это могло бы работать с условным 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 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

Я знаю, что это не поддерживается в С#, но поддерживается в 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 должны быть разные перегрузки, зависящие от количества аргументов (в этом случае my просто использует 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 - другие люди, приходящие в код в первый раз, должны будут копаться, чтобы точно понять, что происходит.

Re-бросок

Код (обратите внимание на 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 сохраняет трассировку стека, отладчик разбивается на линию, где происходит бросок, а не исходный бросок. Совершенно очевидно, что так происходит, но это не то, что я хочу. Это означает, что мне нужно заглянуть внутрь исключения для stacktrace, а затем найти правильную строку кода. Кроме того, теряется состояние локальных переменных, в которых произошло исключение.

Метод обертки

В принципе, просто заверните 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, но я хочу включить поведение для определенных частей кода, а не для конкретных исключений. Кроме того, это должно работать во всех установках.
  • Ассамблея ИЛ. Я рассмотрел inline IL как возможность генерировать условное исключение, но для этого требуются шаги после сборки с сторонними инструментами.
  • Я не думаю, что глобальные (прикладные) обработчики исключений будут делать это, потому что исключение должно быть уловлено и зарегистрировано ниже в стеке приложений.

Обновление - 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();
    }
}

Единственным недостатком является то, что вы действительно хотите войти в код, отмеченный как DebuggerStepThrough или если в этом коде есть исключение. Хотя, это незначительный недостаток, потому что вы можете вообще сохранить этот код минимальным.

Обратите внимание на использование Debugger.IsAttached потому что я думаю, что его влияние здесь минимально и вероятность появления странных heisenbugs минимальна, но остерегайтесь использовать его, как указано Guillaume в комментариях, и использовать другой вариант, например, настройку конфигурации.

Я пойду с этим, если не будет лучшего способа, или кто-то вызывает озабоченность по поводу этого.

Ответ 1

Исключительные фильтры (С# 6+)

Если вы используете С# 6, это легко сделать с новым синтаксисом фильтра исключений:

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

Ответ 2

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 wrapper, которая фактически поддерживает условные блоки 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 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 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 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 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 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 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; }
}

Итоговые заметки

Это огромное редактирование для моего оригинального сообщения. Посмотрите историю изменений для моего первоначального решения.

Ответ 3

Вы можете обернуть исключение и поймать определенный тип исключения, таким образом, при отладке не будет определено поведение 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)
    {
    }
}