Как сбросить InnerException без потери трассировки стека в C #?

305

Я размышляю о методе, который может вызвать исключение. Как я могу передать исключение своему вызывающему абоненту без отражения от оболочки?
Я перебрасываю 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;
    }
}
skolima
источник
1
Есть еще один способ сделать это, не требуя вуду. Посмотрите на ответ здесь: stackoverflow.com/questions/15668334/…
Тимоти Шилдс
Исключение, генерируемое в динамически вызываемом методе, является внутренним исключением исключения «Исключение было сгенерировано целью вызова». У этого есть свой собственный след стека. Там действительно не о чем беспокоиться.
ajeh

Ответы:

481

В .NET 4.5 теперь есть ExceptionDispatchInfoкласс.

Это позволяет вам захватить исключение и повторно выдать его без изменения трассировки стека:

try
{
    task.Wait();
}
catch(AggregateException ex)
{
    ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
}

Это работает на любое исключение, а не только AggregateException.

Он был введен благодаря функции awaitязыка C #, которая разворачивает внутренние исключения из AggregateExceptionэкземпляров, чтобы сделать функции асинхронного языка более похожими на функции синхронного языка.

Пол Тернер
источник
11
Хороший кандидат на метод расширения Exception.Rethrow ()?
nmarler
8
Обратите внимание, что класс ExceptionDispatchInfo находится в пространстве имен System.Runtime.ExceptionServices и недоступен до .NET 4.5.
Йо
53
Вам может потребоваться поставить регулярное throw;после строки .Throw (), потому что компилятор не будет знать, что .Throw () всегда выдает исключение. throw;никогда не будет вызываться в результате, но по крайней мере компилятор не будет жаловаться, если ваш метод требует возвращаемый объект или является асинхронной функцией.
Тодд
5
@Taudris Этот вопрос конкретно касается повторного выброса внутреннего исключения, которое не может быть специально обработано throw;. Если вы используете throw ex.InnerException;трассировку стека, он повторно инициализируется в точке, где он перебрасывается.
Пол Тернер
5
@amitjhaExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw();
Ведран
86

Это является возможным сохранение трассировки стека перед тем Повторное выбрасывание без отражения:

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
2
На самом деле, это не намного медленнее, чем вызов InternalPreserveStackTrace(примерно на 6% медленнее при 10000 итерациях). Прямой доступ к полям с помощью отражения примерно на 2,5% быстрее, чем вызовInternalPreserveStackTrace
Томас Левеск
1
Я бы порекомендовал использовать e.Dataсловарь со строкой или уникальным ключом объекта ( static readonly object myExceptionDataKey = new object ()но не делайте этого, если вам нужно где-либо сериализовать исключения). Избегайте модификаций e.Message, потому что где-то может быть код, который анализирует e.Message. Разбор e.Message- это зло, но другого выбора может не быть, например, если вам нужно использовать стороннюю библиотеку с плохой практикой исключений.
Антон Тихий
10
DoFixups ломается для пользовательских исключений, если у них нет сериализации ctor
ruslander
3
Предлагаемое решение не работает, если исключение не имеет конструктора сериализации. Я предлагаю использовать решение, предложенное на stackoverflow.com/a/4557183/209727, которое хорошо работает в любом случае. Для .NET 4.5 рассмотрим использование класса ExceptionDispatchInfo.
Давиде Икарди
33

Я думаю, что вам лучше всего поставить это в свой блок catch:

throw;

А потом извлекать неустражение позже.

GEOCHET
источник
21
Или вообще удалите попытку / улов.
Даниэль Эрвикер
6
@Earwicker. Удаление try / catch в общем случае не является хорошим решением, поскольку оно игнорирует случаи, когда требуется код очистки перед распространением исключения в стек вызовов.
Иордания
12
@Jordan - Код очистки должен быть в блоке finally, а не в блоке catch
Паоло
17
@Paolo - если это предполагается выполнить в каждом случае, да. Если предполагается выполнение только в случае сбоя, нет.
chiccodoro
4
Имейте в виду, что InternalPreserveStackTrace не является потокобезопасным, поэтому, если у вас есть 2 потока в этих состояниях исключений ... может, Бог помилует нас всех.
Роб
14

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

jeuoekdcwzfwccu
источник
5
Я всегда думал, что «throw» не сбрасывает трассировку стека (в отличие от «throw e»).
Джеспер
@JesperMatthiesen Я могу ошибаться, но я слышал, что это зависит от того, было ли исключение выброшено и перехвачено в одном и том же файле. Если это тот же файл, трассировка стека будет потеряна, если это другой файл, он будет сохранен.
19
13
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: в бета-версии они снова вернули реализацию!
Сэмюэль Джек
3
предложение: измените PreserveStackTrace так, чтобы оно возвращало ex -, затем, чтобы выдать исключение, вы можете просто сказать: throw ex.PreserveStackTrace ();
Simon_Weaver
Зачем использовать Action<Exception>? Здесь используется статический метод
Kiquenet
10

Еще больше размышлений ...

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. Смотрите дальнейшее обсуждение Mono Bugzilla .

skolima
источник
28
Это действительно очень плохая идея, так как она зависит от внутренних недокументированных деталей о классах фреймворка.
Даниэль Эрвикер
1
Оказывается, можно сохранить трассировку стека без отражения, см. Ниже.
Антон Тихий
1
Вызов внутреннего InternalPreserveStackTraceметода был бы лучше, поскольку он делает то же самое и с меньшей вероятностью изменится в будущем ...
Томас Левеск
1
На самом деле, было бы хуже, так как InternalPreserveStackTrace не существует в Mono.
Сколима
5
@daniel - это действительно очень плохая идея для броска; сбросить трассировку стека, когда каждый разработчик .net обучен верить, что это не так. Это также очень, очень, очень плохая вещь, если вы не можете найти источник NullReferenceException и потерять клиента / заказ, потому что не можете его найти. для меня это превосходит «недокументированные детали» и определенно моно.
Simon_Weaver
10

Первое: не теряйте исключение TargetInvocationException - это ценная информация, когда вы захотите отладить что-либо
Во-вторых: оберните TIE как InnerException в свой собственный тип исключения и поместите свойство OriginalException, которое ссылается на то, что вам нужно (и сохраните весь стек вызовов без изменений).
Третье: пусть TIE вспыхнет из вашего метода.

KOKOS
источник
5

Ребята, вы крутые .. Я скоро стану некромантом.

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

    }
Борис Треухов
источник
1
Хорошая идея, но вы не всегда управляете кодом, который вызывает .Invoke().
Антон Тихий
1
И вы не всегда знаете типы аргументов / результата во время компиляции.
Роман Старков
3

Другой пример кода, который использует сериализацию / десериализацию исключений. Это не требует, чтобы фактический тип исключения был сериализуемым. Также он использует только публичные / защищенные методы.

    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 });
    }
chickenbyproduct
источник
не требует, чтобы фактический тип исключения был сериализуемым?
Kiquenet
3

Основываясь на ответе Пола Тернерса, я сделал метод расширения

    public static Exception Capture(this Exception ex)
    {
        ExceptionDispatchInfo.Capture(ex).Throw();
        return ex;
    }

return exIST никогда не достиг , но преимущество заключается в том, что я могу использовать в 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();
        }
    }
Юрген Стейнблок
источник