Некорректная трассировка стека при повторном отбрасывании

Я перебрасываю исключение с помощью throw, но трассировка стека неверна:

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

Правильная трассировка стека должна быть:

System.Exception: Test
   at ConsoleApplication1.Program.Main(String[] args) in Program.cs:Line 12

Но я получаю:

System.Exception: Test
   at ConsoleApplication1.Program.Main(String[] args) in Program.cs:Line 15

Но строка 15 - это позиция "броска"; Я проверил это с.NET 3.5.

12 ответов

Решение

Бросание дважды в одном и том же методе, вероятно, является особым случаем - я не смог создать трассировку стека, где разные строки в одном и том же методе следуют друг за другом. Как говорится в слове, "трассировка стека" показывает вам кадры стека, через которые прошло исключение. И есть только один кадр стека на вызов метода!

Если вы бросаете из другого метода, throw; не удалит запись для Foo(), как и ожидалось:

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

Если вы измените Rethrower() и заменить throw; от throw ex;, Foo() запись в трассировке стека исчезает. Опять же, это ожидаемое поведение.

Это то, что можно считать ожидаемым. Изменение трассировки стека является обычным случаем, если вы укажете throw ex;, FxCop уведомит вас, что стек изменен. В случае, если вы делаете throw;предупреждение не генерируется, но трасса все равно будет изменена. Так что, к сожалению, пока лучше не ловить бывшего или бросать его как внутреннего. Я думаю, что это следует рассматривать как воздействие Windows или что-то подобное - отредактировано.Джефф Рихтер описывает эту ситуацию более подробно в своем "CLR via C#":

Следующий код генерирует тот же объект исключения, который он перехватил, и заставляет CLR сбрасывать свою начальную точку для исключения:

private void SomeMethod() {
  try { ... }
  catch (Exception e) {
    ...
    throw e; // CLR thinks this is where exception originated.
    // FxCop reports this as an error
  }
}

Напротив, если вы перебрасываете объект исключения, используя само ключевое слово throw, CLR не сбрасывает начальную точку стека. Следующий код повторно генерирует тот же объект исключения, который он перехватил, в результате чего CLR не сбрасывает свою начальную точку для исключения:

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

Фактически единственное различие между этими двумя фрагментами кода заключается в том, что, по мнению CLR, является исходным местоположением, в котором было сгенерировано исключение. К сожалению, когда вы выбрасываете или перебрасываете исключение, Windows сбрасывает начальную точку стека. Таким образом, если исключение становится необработанным, местоположение стека, которое сообщается в службу отчетов об ошибках Windows, является местоположением последнего выброса или повторного выброса, даже если CLR знает местоположение стека, в которое было сгенерировано исходное исключение. Это прискорбно, потому что это делает отладку приложений, которые потерпели неудачу в полевых условиях, намного сложнее. Некоторые разработчики сочли это настолько невыносимым, что выбрали другой способ реализации своего кода, чтобы гарантировать, что трассировка стека действительно отражает местоположение, где изначально было выдано исключение:

private void SomeMethod() {
  Boolean trySucceeds = false;
  try {
    ...
    trySucceeds = true;
  }
  finally {
    if (!trySucceeds) { /* catch code goes in here */ }
  }
}

Это хорошо известное ограничение в версии CLR для Windows. Он использует встроенную поддержку Windows для обработки исключений (SEH). Проблема в том, что он основан на кадрах стека, а метод имеет только один кадр стека. Вы можете легко решить эту проблему, переместив внутренний блок try / catch в другой вспомогательный метод, создав тем самым еще один кадр стека. Еще одним следствием этого ограничения является то, что JIT-компилятор не встроит ни один метод, содержащий оператор try.

Как я могу сохранить НАСТОЯЩУЮ трассировку стека?

Вы бросаете новое исключение и включаете исходное исключение в качестве внутреннего исключения.

но это некрасиво... дольше... делает выбор за исключением броска....

Вы не правы в отношении уродливых, но правы в отношении двух других пунктов. Эмпирическое правило таково: не поймайте, если вы не собираетесь с ним что-то делать, например, обернуть его, изменить, проглотить или записать в журнал. Если вы решили catch а потом throw опять же, убедитесь, что вы что-то делаете с этим, иначе просто дайте пузыриться.

У вас также может возникнуть желание поместить перехватчик просто для того, чтобы вы могли установить точку останова внутри перехвата, но в отладчике Visual Studio есть достаточно параметров, чтобы сделать эту практику ненужной, попробуйте вместо этого использовать исключения первого шанса или условные точки останова.

Редактировать / Заменить

Поведение на самом деле другое, но так тонко. Что касается того, почему поведение отличается, мне нужно обратиться к эксперту CLR.

РЕДАКТИРОВАТЬ: Ответ AlexD, кажется, указывает, что это сделано намеренно.

Бросок исключения в том же методе, который его перехватывает, немного запутывает ситуацию, поэтому давайте выбросим исключение из другого метода:

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

Если throw; используется стек вызовов (номера строк заменяются кодом):

at Throw():line (int b = 10 / a;)
at Main():line (throw;) // This has been modified

Если throw ex; используется, callstack:

at Main():line (throw ex;)

Если исключение не обнаружено, стек вызовов:

at Throw():line (int b = 10 / a;)
at Main():line (Throw())

Протестировано в.NET 4 / VS 2010

Вы можете сохранить трассировку стека, используя

ExceptionDispatchInfo.Capture(ex);

Вот пример кода:

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

Результат будет примерно таким:

Тестовое приложение ex
Тест внутренний экс
   в TestApp.Program.CallAndThrow() в D:\Projects\TestApp\TestApp\Program.cs: строка 19
   в TestApp.Program.Main(String[] args) в D:\Projects\TestApp\TestApp\Program.cs: строка 30
--- Конец стека трассировки от предыдущего местоположения, где было сгенерировано исключение ---
   в System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   в TestApp.Program.Main(String[] args) в D:\Projects\TestApp\TestApp\Program.cs: строка 38
--- Конец стека трассировки от предыдущего местоположения, где было сгенерировано исключение ---
   в System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   в TestApp.Program.Main(String[] args) в D:\Projects\TestApp\TestApp\Program.cs: строка 47

Здесь есть дублирующий вопрос.

Как я понимаю - брось; компилируется в инструкцию MSI 'rethrow' и модифицирует последний кадр трассировки стека.

Я ожидал бы, что он сохранит исходную трассировку стека и добавит строку, в которую он был переброшен, но, очевидно, может быть только один кадр стека на вызов метода.

Вывод: избегайте использования броска; и оберните свое исключение в новое при повторном броске - это не уродливо, это лучшая практика.

ОК, в.NET Framework, похоже, есть ошибка, если вы генерируете исключение и перебрасываете его тем же методом, исходный номер строки теряется (это будет последняя строка метода).

К счастью, умный парень по имени Фабрис МАРЖУРИ нашел решение этой ошибки. Ниже моя версия, которую вы можете протестировать в этой.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;
}

Теперь ловите исключение как обычно, но вместо броска; просто вызовите этот метод, и вуаля, оригинальный номер строки будет сохранен!

Не уверен, что это так, но я думаю, что так было всегда.

Если исходное значение throw для нового Exception находится в отдельном методе, то результат для throw должен иметь исходное имя метода и номер строки, а затем номер строки в main, где выдается исключение.

Если вы используете throw ex, то результатом будет просто строка в main, где исключение - rethrow.

Другими словами, throw ex теряет всю трассировку стека, в то время как throw сохраняет историю трассировки стека (т.е. детали методов более низкого уровня). Но если ваше исключение сгенерировано тем же методом, что и повторный выброс, вы можете потерять некоторую информацию.

NB. Если вы пишете очень простую и небольшую тестовую программу, Framework может иногда оптимизировать вещи и менять метод на встроенный код, что означает, что результаты могут отличаться от "реальной" программы.

Вы хотите, чтобы ваш правильный номер строки? Просто используйте один метод try/catch для каждого метода. В системах, ну... просто на уровне пользовательского интерфейса, а не на уровне логики или доступа к данным, это очень раздражает, потому что, если вам нужны транзакции с базой данных, их не должно быть на уровне пользовательского интерфейса, и у вас не будет правильный номер строки, но если они вам не нужны, не перебрасывайте ни с без исключения в catch...

Примерный код за 5 минут:

Файл меню -> Новый проект, поместите три кнопки и вызовите следующий код в каждой:

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

Теперь создайте новый класс:

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

Беги... первая кнопка красивая!

Я думаю, что это не столько случай изменения трассировки стека, а скорее способ определения номера строки для трассировки стека. Испытание в Visual Studio 2010, поведение похоже на то, что вы ожидаете от документации MSDN: "throw ex;" перестраивает трассировку стека с точки этого оператора "throw;" оставляет трассировку стека такой, какой она есть, за исключением того, что когда бы ни возникало исключение, номер строки - это место повторного выброса, а не вызов, через который прошло исключение.

Так что с "бросить"; дерево вызовов метода остается неизменным, но номера строк могут измениться.

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

Как говорили многие другие люди, обычно лучше не улавливать исключение, если только вам это не нужно, и / или вы собираетесь разобраться с ним на этом этапе.

Интересное примечание: Visual Studio 2010 даже не позволяет мне создавать код, представленный в вопросе, так как он обнаруживает ошибку деления на ноль во время компиляции.

Это потому, что вы поймали Exception из линии 12 и перебросили его на линию 15, так что трассировка стека принимает это как наличные деньги, что Exception был брошен оттуда.

Чтобы лучше обрабатывать исключения, вы должны просто использовать try...finallyи пусть необработанный Exception пузыриться.

Другие вопросы по тегам