Неправильная трассировка стека при повторном отбрасывании - PullRequest
36 голосов
/ 18 ноября 2010

Я перебрасываю исключение с помощью «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 - это позиция "throw;". Я проверил это с .NET 3.5.

Ответы [ 12 ]

27 голосов
/ 18 ноября 2010

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

Если вы выбросите из другого метода, 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() в трассировке стека исчезнет. Опять же, это ожидаемое поведение.

24 голосов
/ 19 января 2011

Это то, что можно считать ожидаемым.Изменение трассировки стека является обычным случаем, если вы укажете 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, является местоположением последнего выброса или повторного запроса-throw, даже если CLR знает расположение стека, в которое было сгенерировано исходное исключение.Это прискорбно, потому что это затрудняет отладку приложений, которые потерпели неудачу в полевых условиях.Некоторые разработчики сочли это настолько невыносимым, что выбрали другой способ реализации своего кода, чтобы гарантировать, что трассировка стека действительно отражает местоположение, в которое изначально было выдано исключение:

private void SomeMethod() {
  Boolean trySucceeds = false;
  try {
    ...
    trySucceeds = true;
  }
  finally {
    if (!trySucceeds) { /* catch code goes in here */ }
  }
}
19 голосов
/ 18 ноября 2010

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

10 голосов
/ 19 января 2011

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

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

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

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

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

7 голосов
/ 19 января 2011

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

Поведение на самом деле другое, но тонко. Что касается почему поведение, если оно отличается, мне нужно обратиться к эксперту 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;, стек вызовов:

at Main():line (throw ex;)

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

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

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

5 голосов
/ 19 января 2011

Повторяющийся вопрос здесь .

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

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

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

4 голосов
/ 12 июля 2016

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

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

Вывод будет примерно таким:

Test app ex
Test inner ex
   at TestApp.Program.CallAndThrow() in D:\Projects\TestApp\TestApp\Program.cs:line 19
   at TestApp.Program.Main(String[] args) in D:\Projects\TestApp\TestApp\Program.cs:line 30
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at TestApp.Program.Main(String[] args) in D:\Projects\TestApp\TestApp\Program.cs:line 38
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at TestApp.Program.Main(String[] args) in D:\Projects\TestApp\TestApp\Program.cs:line 47
3 голосов
/ 05 февраля 2014

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

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

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

2 голосов
/ 18 ноября 2010

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

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

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

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

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

1 голос
/ 25 июля 2012

Хотите ли вы правильный номер строки? Просто используйте one 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;
        }
    }
}

Запустить ... первая кнопка прекрасна!

...