Як перезавантажити 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;
    }
}

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.
yoyo

53
Можливо, вам потрібно буде поставити звичайний throw;рядок після .Throw (), тому що компілятор не знатиме, що .Throw () завжди видає виняток. throw;ніколи не буде викликано як результат, але принаймні компілятор не поскаржиться, якщо ваш метод вимагає повернення об'єкта або функція асинхронізації.
Тодд

5
@Taudris Це питання спеціально стосується повторного скидання внутрішнього виключення, яке не може бути спеціально вирішене throw;. Якщо ви використовуєте, throw ex.InnerException;слід стека повторно ініціалізується в точці, яку він повторно кидає.
Пол Тернер

5
@amitjhaExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw();
Vedran

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

3
Пропоноване рішення не працює, якщо у винятку немає конструктора серіалізації. Я пропоную використовувати рішення, запропоноване на сайті stackoverflow.com/a/4557183/209727, яке добре працює в будь-якому випадку. Для .NET 4.5 рекомендуємо використовувати клас ExceptionDispatchInfo.
Девід Ікарді

33

Я думаю, що найкраще було б просто поставити це у свій блок лову:

throw;

А потім витягнути іннерецепцію пізніше.


21
Або видаліть спробу / зловити зовсім.
Даніель Ервікер

6
@Earwicker. Видалення спроби / лову не є хорошим рішенням загалом, оскільки воно ігнорує випадки, коли потрібний код очищення перед розповсюдженням виключення до стеку викликів.
Йорданія

12
@Jordan - Очищення коду повинно бути, нарешті, блоком, а не блоком лову
Паоло

17
@Paolo - Якщо це передбачається виконати у кожному випадку, так. Якщо вона повинна бути виконана лише у випадку відмови, ні.
чікодоро

4
Майте на увазі, що InternalPreserveStackTrace не є безпечною ниткою, тому, якщо у вас є дві нитки з цих станів винятків ... хай Бог помилує нас усіх.
Роб

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.PreserveStackTrace ();
Simon_Weaver

Навіщо використовувати Action<Exception>? Тут використовується статичний метод
Кікенет

13

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


4
Я завжди вважав, що "кидок" не скидає стеження (на відміну від "кинути е").
Jesper Matthiesen

@JesperMatthiesen Можливо, я помилився, але почув, що це залежить, якщо виняток буде кинуто і потрапило в той самий файл. Якщо це той самий файл, слід стека буде втрачено, якщо це інший файл, він буде збережений.
jahu

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 .


28
Це дійсно, дуже погана ідея, оскільки вона залежить від внутрішніх незадокументованих деталей про рамкові заняття.
Даніель Ервікер

1
Виявляється, можна зберегти слід стека без відображення, див. Нижче.
Антон Тихий

1
Викликати внутрішній InternalPreserveStackTraceметод було б краще, оскільки він робить те саме, і менше шансів змінитись у майбутньому ...
Томас Левеск

1
Насправді це було б гірше, оскільки InternalPreserveStackTrace не існує на Mono.
Сколіма

5
@daniel - ну це дуже, дуже, дуже погана ідея для кидка; щоб скинути стек-трек, коли кожен розробник .net навчається вірити, що цього не зробить. це також дуже, дуже, дуже погана річ, якщо ви не можете дізнатись джерело NullReferenceException і втратити клієнта / замовлення, тому що ви не можете його знайти. для мене, що козовить "незадокументовані деталі" і, безумовно, моно.
Simon_Weaver

10

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


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

не вимагає, щоб фактичний тип винятку був серіалізаційним?
Кікенет

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();
        }
    }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.