Я вызываю через отражение метод, который может вызвать исключение. Как я могу передать исключение вызывающему абоненту без того, чтобы его окружало отражение оболочки?
Я повторно генерирую InnerException, но это уничтожает трассировку стека.
Пример кода:
public void test1()
{
// Throw an exception for testing purposes
throw new ArgumentException("test1");
}
void test2()
{
try
{
MethodInfo mi = typeof(Program).GetMethod("test1");
mi.Invoke(this, null);
}
catch (TargetInvocationException tiex)
{
// Throw the new exception
throw tiex.InnerException;
}
}
Исключение, созданное в динамически вызываемом методе, является внутренним исключением исключения «Исключение было создано целью вызова». У него есть собственная трассировка стека. Больше не о чем беспокоиться.





Я думаю, что лучше всего было бы просто поместить это в свой блок catch:
throw;
А потом извлеките внутреннее исключение.
Или вообще удалите try / catch.
@Earwicker. Удаление try / catch в целом не является хорошим решением, поскольку игнорирует случаи, когда требуется код очистки перед распространением исключения вверх по стеку вызовов.
@Jordan - код очистки должен быть в блоке finally, а не в блоке catch
@Paolo - Если предполагается, что это должно выполняться в каждом случае, да. Если предполагается, что он будет выполняться только в случае неудачи, нет.
Имейте в виду, что InternalPreserveStackTrace не является потокобезопасным, поэтому, если у вас есть 2 потока в одном из этих состояний исключения ... да помилует нас всех Бог.
Это работает, если вы "бросаете" сразу в блок catch, но не работает, если вам нужно "выбросить" за пределы блока catch.
Еще больше размышлений ...
catch (TargetInvocationException tiex)
{
// Get the _remoteStackTraceString of the Exception class
FieldInfo remoteStackTraceString = typeof(Exception)
.GetField("_remoteStackTraceString",
BindingFlags.Instance | BindingFlags.NonPublic); // MS.Net
if (remoteStackTraceString == null)
remoteStackTraceString = typeof(Exception)
.GetField("remote_stack_trace",
BindingFlags.Instance | BindingFlags.NonPublic); // Mono
// Set the InnerException._remoteStackTraceString
// to the current InnerException.StackTrace
remoteStackTraceString.SetValue(tiex.InnerException,
tiex.InnerException.StackTrace + Environment.NewLine);
// Throw the new exception
throw tiex.InnerException;
}
Имейте в виду, что это может сломаться в любой момент, поскольку частные поля не являются частью API. См. Дальнейшее обсуждение Моно багзилла.
Это действительно очень плохая идея, поскольку она зависит от внутренних недокументированных деталей о классах фреймворка.
@Earwicker: +1 это действительно плохая идея
Оказывается, можно сохранить трассировку стека без отражения, см. Ниже.
Было бы лучше вызвать внутренний метод InternalPreserveStackTrace, поскольку он делает то же самое и вряд ли изменится в будущем ...
На самом деле, было бы хуже, поскольку InternalPreserveStackTrace не существует на Mono.
@daniel - ну, это действительно, очень, очень плохая идея для броска; чтобы сбросить трассировку стека, когда каждый разработчик .net убежден, что это не так. Это также действительно, очень, очень плохо, если вы не можете узнать источник исключения NullReferenceException и потерять клиента / заказ, потому что вы не можете его найти. для меня это важнее «недокументированных деталей» и определенно моно.
@Simon: throw; не сбрасывает трассировку стека. throw e; делает.
Ага, это плохо. Да, это взлом. Да, может сломаться. Но напишите для него модульный тест, и если он выйдет из строя в каком-то будущем обновлении .NET, вы найдете его до того, как выпустите его для клиентов, и все, что у вас будет, - это время в будущем для разработки нового хака.
Во-первых: не теряйте TargetInvocationException - это ценная информация, когда вы захотите что-то отладить. Во-вторых: оберните TIE как InnerException в свой собственный тип исключения и поместите свойство OriginalException, которое ссылается на то, что вам нужно (и сохраните весь стек вызовов нетронутым). В-третьих: позвольте TIE пузырю исчезнуть из вашего метода.
public static class ExceptionHelper
{
private static Action<Exception> _preserveInternalException;
static ExceptionHelper()
{
MethodInfo preserveStackTrace = typeof( Exception ).GetMethod( "InternalPreserveStackTrace", BindingFlags.Instance | BindingFlags.NonPublic );
_preserveInternalException = (Action<Exception>)Delegate.CreateDelegate( typeof( Action<Exception> ), preserveStackTrace );
}
public static void PreserveStackTrace( this Exception ex )
{
_preserveInternalException( ex );
}
}
Вызовите метод расширения для своего исключения, прежде чем его выбросить, он сохранит исходную трассировку стека.
Имейте в виду, что в .Net 4.0 InternalPreserveStackTrace теперь не работает - посмотрите в Reflector, и вы увидите, что метод полностью пуст!
Счеркните это: я смотрел на RC: в бета-версии они снова вернули реализацию!
предложение: измените PreserveStackTrace, чтобы он возвращал ex - затем, чтобы выбросить исключение, вы можете просто сказать: throw ex.PreserveStackTrace ();
Зачем использовать Action<Exception>? Здесь использовать статический метод
Ребята, вы классные .. Я скоро стану некромантом.
public void test1()
{
// Throw an exception for testing purposes
throw new ArgumentException("test1");
}
void test2()
{
MethodInfo mi = typeof(Program).GetMethod("test1");
((Action)Delegate.CreateDelegate(typeof(Action), mi))();
}
Хорошая идея, но вы не всегда контролируете код, который вызывает .Invoke().
И вы не всегда знаете типы аргументов / результатов во время компиляции.
является возможно сохранить трассировку стека перед повторным выбросом без отражения:
static void PreserveStackTrace (Exception e)
{
var ctx = new StreamingContext (StreamingContextStates.CrossAppDomain) ;
var mgr = new ObjectManager (null, ctx) ;
var si = new SerializationInfo (e.GetType (), new FormatterConverter ()) ;
e.GetObjectData (si, ctx) ;
mgr.RegisterObject (e, 1, si) ; // prepare for SetObjectData
mgr.DoFixups () ; // ObjectManager calls SetObjectData
// voila, e is unmodified save for _remoteStackTraceString
}
Это тратит много циклов по сравнению с вызовом InternalPreserveStackTrace через кешированный делегат, но имеет то преимущество, что полагается только на общедоступные функции. Вот несколько распространенных шаблонов использования функций сохранения трассировки стека:
// usage (A): cross-thread invoke, messaging, custom task schedulers etc.
catch (Exception e)
{
PreserveStackTrace (e) ;
// store exception to be re-thrown later,
// possibly in a different thread
operationResult.Exception = e ;
}
// usage (B): after calling MethodInfo.Invoke() and the like
catch (TargetInvocationException tiex)
{
PreserveStackTrace (tiex.InnerException) ;
// unwrap TargetInvocationException, so that typed catch clauses
// in library/3rd-party code can work correctly;
// new stack trace is appended to existing one
throw tiex.InnerException ;
}
Выглядит круто, что должно произойти после запуска этих функций?
@vdboor: Я не совсем понимаю ваш вопрос. Редактирование прояснило ситуацию?
На самом деле, это ненамного медленнее, чем вызов InternalPreserveStackTrace (примерно на 6% медленнее при 10000 итерациях). Доступ к полям напрямую через отражение примерно на 2,5% быстрее, чем при вызове InternalPreserveStackTrace.
@Thomas: при использовании InternalPreserveStackTrace гораздо быстрее создать делегата для него, вместо того, чтобы каждый раз вызывать с отражением. Я сравнил с первым сценарием.
Привет! Спасибо за отличный код и пояснения. Можно ли добавить строку в сообщение об исключении, чтобы я мог фиксировать не только трассировку стека, откуда она была выброшена, но и причину?
Я бы рекомендовал использовать словарь e.Data со строкой или уникальным ключом объекта (static readonly object myExceptionDataKey = new object (), но не делайте этого, если вам нужно где-то сериализовать исключения). Избегайте изменения e.Message, потому что у вас может быть где-то код, который анализирует e.Message. Парсить e.Message - зло, но другого выхода может и не быть, например если вам нужно использовать стороннюю библиотеку с плохой практикой исключения.
DoFixups прерывается для пользовательских исключений, если у них нет ctor сериализации
Предлагаемое решение не работает, если исключение не имеет конструктора сериализации. Я предлагаю использовать решение, предложенное на stackoverflow.com/a/4557183/209727, которое хорошо работает в любом случае. Для .NET 4.5 рассмотрите возможность использования класса ExceptionDispatchInfo.
Так что, это. В своем ответе я упоминаю, что преимущество подхода сериализации заключается в том, что он полагается только на общедоступные API. Очевидно, некоторые стандарты проверки кода не одобряют использование отражения для доступа к внутренним компонентам среды CLR. Что касается класса ExceptionDispatchInfo, он хорош, но не сериализуем (вероятно, потому, что он сохраняет информацию Watson в дополнение к трассировке стека), что несколько ограничивает его полезность.
Ответ stackoverflow.com/a/9989557/206730не требовать тип исключения должен быть сериализуемый?
Другой пример кода, который использует сериализацию / десериализацию исключений. Это не требует, чтобы фактический тип исключения был сериализуемым. Также он использует только общедоступные / защищенные методы.
static void PreserveStackTrace(Exception e)
{
var ctx = new StreamingContext(StreamingContextStates.CrossAppDomain);
var si = new SerializationInfo(typeof(Exception), new FormatterConverter());
var ctor = typeof(Exception).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeof(SerializationInfo), typeof(StreamingContext) }, null);
e.GetObjectData(si, ctx);
ctor.Invoke(e, new object[] { si, ctx });
}
не требовать, чтобы фактический тип исключения был сериализуемым?
В .NET 4.5 теперь есть класс ExceptionDispatchInfo.
Это позволяет вам захватить исключение и повторно выбросить его без изменения трассировки стека:
try
{
task.Wait();
}
catch(AggregateException ex)
{
ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
}
Это работает с любым исключением, а не только с AggregateException.
Он был введен благодаря функции языка C# await, которая разворачивает внутренние исключения из экземпляров AggregateException, чтобы сделать функции асинхронного языка более похожими на функции синхронного языка.
Хороший кандидат для метода расширения Exception.Rethrow ()?
Обратите внимание, что класс ExceptionDispatchInfo находится в пространстве имен System.Runtime.ExceptionServices и недоступен до .NET 4.5.
Вам может потребоваться поставить обычный throw; после строки .Throw (), потому что компилятор не будет знать, что .Throw () всегда генерирует исключение. throw; никогда не будет вызван в результате, но, по крайней мере, компилятор не будет жаловаться, если ваш метод требует возвращаемого объекта или является асинхронной функцией.
Есть ли реализация для .Net 4.0?
Почему простого throw; недостаточно? Я только что протестировал его в небольшом консольном приложении и обнаружил, что трассировка стека не изменилась между повторным генерированием и отсутствием обработки исключения.
@Taudris Этот вопрос конкретно касается повторной генерации внутреннего исключения, которое не может быть специально обработано throw;. Если вы используете throw ex.InnerException;, трассировка стека повторно инициализируется в момент повторного вызова.
Упс, пропустил деталь InnerException. Неважно, имеет смысл! Это могло быть весьма полезно.
иногда InnerException имеет значение null. Что в таком случае?
@amitjha в случае, если InnerException имеет значение null, внешнее исключение должно быть просто сгенерировано.
@amitjha ExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw();
throw;та же, что System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(ex).Throw(); и сохраняет трассировку стека?
public static T Rethrow<T>(this System.Exception ex){ ExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw();throw new System.Exception(); // noop} Это позволяет вернуться, чтобы сделать компиляцию счастливой. Не уверен, что это отличная идея ...
@RedRidingHood, хорошо, но в Generic нет необходимости, верно? Просто недействительно.
Никто не объяснил разницу между ExceptionDispatchInfo.Capture( ex ).Throw() и простым throw, так что вот она.
Полный способ повторно вызвать перехваченное исключение - использовать ExceptionDispatchInfo.Capture( ex ).Throw() (доступно только в .Net 4.5).
Ниже приведены примеры, необходимые для проверки:
1.
void CallingMethod()
{
//try
{
throw new Exception( "TEST" );
}
//catch
{
// throw;
}
}
2.
void CallingMethod()
{
try
{
throw new Exception( "TEST" );
}
catch( Exception ex )
{
ExceptionDispatchInfo.Capture( ex ).Throw();
throw; // So the compiler doesn't complain about methods which don't either return or throw.
}
}
3.
void CallingMethod()
{
try
{
throw new Exception( "TEST" );
}
catch
{
throw;
}
}
4.
void CallingMethod()
{
try
{
throw new Exception( "TEST" );
}
catch( Exception ex )
{
throw new Exception( "RETHROW", ex );
}
}
Случай 1 и случай 2 предоставят вам трассировку стека, где номер строки исходного кода для метода CallingMethod - это номер строки строки throw new Exception( "TEST" ).
Однако вариант 3 предоставит вам трассировку стека, где номер строки исходного кода для метода CallingMethod - это номер строки вызова throw. Это означает, что если строка throw new Exception( "TEST" ) окружена другими операциями, вы не знаете, на каком номере строки фактически было сгенерировано исключение.
Случай 4 аналогичен случаю 2, потому что номер строки исходного исключения сохраняется, но не является реальным повторным вызовом, поскольку он изменяет тип исходного исключения.
Я всегда думал, что «throw» не сбрасывает трассировку стека (в отличие от «throw e»).
@JesperMatthiesen Я могу ошибаться, но я слышал, что это зависит от того, было ли выброшено исключение и обнаружено ли оно в том же файле. Если это тот же файл, трассировка стека будет потеряна, если это другой файл, она будет сохранена.
То, что я могу найти на ExceptionDispatchInfo, указывает на его использование, когда вы хотите повторно выбросить вне контекста улова или развернуть AggregateException. Разница, которую я вижу в своих тестах, заключается в том, что throw; не может захватить какие-либо строки из try в трассировке стека. Изменив пример 3 на try { DoThrow(); } catch (Exception) { throw; }, вы увидите, что строка выброса в DoThrowявляется включена в трассировку стека, но все же не вызывающая линия из try. Тогда как ExceptionDispatchInfo.Capture включает эту строку. По-прежнему кажется, что слишком много работы за пределами основных вариантов использования.
Поведение из примера 3 было зарегистрировано как ошибка .NET Core и исправлено в .NET Core 2.1: github.com/dotnet/runtime/issues/9518
Основываясь на ответе Пола Тернерса, я сделал метод расширения
public static Exception Capture(this Exception ex)
{
ExceptionDispatchInfo.Capture(ex).Throw();
return ex;
}
return ex никогда не достигается, но преимущество в том, что я могу использовать throw ex.Capture() как однострочник, поэтому компилятор не вызовет ошибку not all code paths return a value.
public static object InvokeEx(this MethodInfo method, object obj, object[] parameters)
{
{
return method.Invoke(obj, parameters);
}
catch (TargetInvocationException ex) when (ex.InnerException != null)
{
throw ex.InnerException.Capture();
}
}
Есть другой способ сделать это, не требующий вуду. Взгляните на ответ здесь: stackoverflow.com/questions/15668334/…