Неправильная трассировка стека при повторном вызове

StackOverflow https://stackoverflow.com/questions/4217616

  •  26-09-2019
  •  | 
  •  

Вопрос

Я повторно создаю исключение с помощью «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.

Это было полезно?

Решение

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

Если вы бросаете из другого метода, 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 через C #":

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

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

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

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

Это хорошо известное ограничение в версии Windows CLR. Он использует встроенную поддержку Windows для обработки исключений (SEH). Проблема в том, что он находится на основе кадра стека, а метод имеет только одну кадру стека. Вы можете легко решить проблему, переместив свой блок Thrue / Catch в другой метод помощника, создавая еще один кадр стека. Другое следствие этого ограничения заключается в том, что Compiler 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; Используется, CallStack - это (линейные номера заменены с кодом):

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

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

at Main():line (throw ex;)

Если исключение не поймано, CaultStack:

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

Проверено в .NET 4 / VS 2010

Есть дубликат вопрос здесь.

Как я понимаю, бросить; скомпилирован в «Rethrow» MSIL Инструкция И это изменяет последний кадр стека-трассировки.

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

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

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

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 in testapp.program.callandthrow () в d:  projects  testapp  testapp  programent.cs: line 19 на testapp.program.main (строка [] args) в d:  projects  testapp  TestApp  Program.cs: линия 30 - конец трассы стека из предыдущего местоположения, где был брошен исключение --- в System.runtime.exceptionsives.RueCeCeptionDispatchinfo.ruow () на testapp.program.main (строка [] args) в D:  projects  testapp  testapp  program.cs: линия 38 - конец следа стека из предыдущего местоположения, где был брошен исключение --- в System.runtime.exceptionsives.ExceptionDISPATCINFO.ROW () на testapp.program.main (Строка [] args) в d:  projects  testapp  testapp  program.cs: строка 47

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

Fortunatelly, умный парень по имени Фабрис Маргери нашел решение к этой ошибке. Ниже приведена моя версия, которую вы можете проверить в Это .NET Widdle.

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

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

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

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

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

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

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

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

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

Так с «броском»; Дерево вызовов на методу остается неизменным, но номера линии могут измениться.

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top