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