Как определить, обрабатывается ли исключение .NET?

Мы исследуем шаблон кодирования на C #, в котором мы хотели бы использовать предложение using со специальным классом, чей метод Dispose() выполняет разные действия в зависимости от того, было ли завершено тело using: нормально или с исключением.

Насколько я понимаю, CLR отслеживает текущее обрабатываемое исключение до тех пор, пока оно не будет обработано обработчиком "catch". Однако не совсем ясно, предоставляется ли эта информация каким-либо образом для доступа кода. Знаете ли вы, есть ли это, и если да, то как получить к нему доступ?

Например:

using (var x = new MyObject())
{
    x.DoSomething();
    x.DoMoreThings();
}

class MyObject : IDisposable
{
    public void Dispose()
    {
        if (ExceptionIsBeingHandled)
            Rollback();
        else
            Commit();
    }
}

Это выглядит почти как System.Transactions.TransactionScope, за исключением того, что успех / неудача определяется не вызовом x.Complete(), а скорее зависит от того, было ли тело using завершено нормально.


person Roman Starkov    schedule 29.11.2009    source источник
comment
Я думаю, вам нужно спросить, почему вы пытаетесь это сделать. Шаблон Dispose () не предназначен для реализации логики управления путем создания исключений.   -  person Mitch Wheat    schedule 29.11.2009
comment
Всегда справедливо подвергнуть сомнению всю идею. Я понимаю, что это не то, как нужно использовать using. Я понимаю, что это может привести к плохому коду. Мне все еще интересен ответ :)   -  person Roman Starkov    schedule 29.11.2009
comment
Почему этот вопрос отклонен?   -  person Olli    schedule 29.11.2009
comment
Оператор using преобразуется в блок try / finally, и объект удаляется.   -  person SoftwareGeek    schedule 29.07.2010


Ответы (5)


http://www.codewrecks.com/blog/index.php/2008/07/25/detecting-if-finally-block-is-executing-for-an-manhandled-exception/ описывает «взлом», чтобы определить, выполняется ли ваш код в режиме обработки исключений или нет. Он использует Marshal.GetExceptionPointers, чтобы узнать, является ли исключение «активным».

Но имейте в виду:

Замечания

GetExceptionPointers предоставляется только для поддержки компилятором структурированной обработки исключений (SEH). Примечание Примечание:

Этот метод использует SecurityAction.LinkDemand для предотвращения его вызова из ненадежного кода; только непосредственный вызывающий должен иметь разрешение SecurityPermissionAttribute.UnmanagedCode. Если ваш код может быть вызван из частично доверенного кода, не передавайте вводимые пользователем данные в методы класса Marshal без проверки. О важных ограничениях на использование члена LinkDemand см. В разделе «Спрос против LinkDemand».

person VolkerK    schedule 29.11.2009
comment
Это, вероятно, единственный реальный ответ на ваш вопрос, но это кажется очень плохой идеей. - person Paul Turner; 29.11.2009
comment
Спасибо, на месте. @ Programming Hero: возможно. Не могу отрицать, что так кажется. Мы попробуем это сделать в небольшом тестовом проекте и посмотрим, нет ли серьезных проблем с этим подходом. - person Roman Starkov; 29.11.2009
comment
Если вы все же пойдете по этому пути, сначала посмотрите следующий блог: geekswithblogs.net/akraus1/archive/2008/04/08/121121.aspx - person Joe; 29.11.2009
comment
romkyns: с таким взломом вы не увидите серьезных проблем в небольшом тестовом проекте. Проблемы возникают тогда, когда ваш код развертывается на машинах с тысячами уникальных и различных особенностей конфигурации. - person Anton Tykhyy; 29.11.2009
comment
Просто замечание для всех, кто думает об использовании этого хака: он остается непроверенным в дикой природе, поскольку мы не использовали его; см. мой пост ниже, чтобы увидеть, что мы сделали вместо этого. - person Roman Starkov; 10.03.2010

Не ответ на вопрос, а просто примечание о том, что я никогда не использовал «принятый» хак в реальном коде, так что он все еще в значительной степени не протестирован «в дикой природе». Вместо этого мы пошли примерно так:

DoThings(x =>
{
    x.DoSomething();
    x.DoMoreThings();
});

где

public void DoThings(Action<MyObject> action)
{
    bool success = false;
    try
    {
        action(new MyObject());
        Commit();
        success = true;
    }
    finally
    {
        if (!success)
            Rollback();
    }
}

Ключевым моментом является то, что он такой же компактный, как пример «использования» в вопросе, и не использует никаких хаков.

Среди недостатков - снижение производительности (совершенно незначительное в нашем случае) и переход F10 в DoThings, когда я действительно хочу, чтобы он просто переходил прямо к x.DoSomething(). Оба очень второстепенные.

person Roman Starkov    schedule 09.03.2010
comment
Это гораздо менее привлекательно в VS2010 из-за ошибок IntelliSense, связанных с анонимными методами ... connect.microsoft.com/VisualStudio/feedback/details/557224/ - person Roman Starkov; 06.05.2010

Эта информация вам недоступна.

Я бы использовал шаблон, аналогичный тому, который используется в классе DbTransaction: то есть ваш класс IDisposable должен реализовывать метод, аналогичный DbTransaction.Commit (). Затем ваш метод Dispose может выполнять различную логику в зависимости от того, был ли вызван Commit (в случае DbTransaction, транзакция будет отменена, если она не была зафиксирована явно).

Затем пользователи вашего класса будут использовать следующий шаблон, аналогичный типичному DbTransaction:

using(MyDisposableClass instance = ...)
{
    ... do whatever ...

    instance.Commit();
} // Dispose logic depends on whether or not Commit was called.

ИЗМЕНИТЬ. Я вижу, что вы изменили свой вопрос, чтобы показать, что вы знаете об этом шаблоне (в вашем примере используется TransactionScope). Тем не менее я считаю, что это единственно реальное решение.

person Joe    schedule 29.11.2009

Оператор using - это просто синтаксический сахар для блока try finally. Вы можете получить то, что хотите, написав, наконец, попытку полностью, а затем добавив оператор catch для обработки вашего особого случая:

try
{
    IDisposable x = new MyThing();
}
catch (Exception exception) // Use a more specific exception if possible.
{
    x.ErrorOccurred = true; // You could even pass a reference to the exception if you wish.
    throw;
}
finally
{
    x.Dispose();
}

Внутри MyThing вы можете сделать это, если хотите, например:

class MyThing : IDisposable
{
    public bool ErrorOccurred() { get; set; }

    public void Dispose()
    {
        if (ErrorOccurred) {
            RollBack();
        } else {
            Commit();
        }
    }
}

Примечание: я также должен задаться вопросом, почему вы хотите это сделать. Есть запах кода. Метод Dispose предназначен для очистки неуправляемых ресурсов, а не для обработки исключений. Вероятно, вам будет лучше написать код обработки исключений в блоке catch, а не в dispose, и если вам нужно поделиться кодом, сделайте несколько полезных вспомогательных функций, которые вы можете вызывать из обоих мест.

Вот лучший способ делать то, что вы хотите:

using (IDisposable x = new MyThing())
{
    x.Foo();
    x.Bar();
    x.CommitChanges();
}

class MyThing : IDisposable
{
    public bool IsCommitted { get; private set; }

    public void CommitChanges()
    {
        // Do stuff needed to commit.
        IsCommitted = true;
    }

    public void Dispose()
    {
        if (!IsCommitted)
            RollBack();
    }
}
person Mark Byers    schedule 29.11.2009
comment
Ах, я должен был упомянуть об этом. Дело в том, что код, который идет в catch и, наконец, идентичен, но тоже нетривиален. Дело в том, чтобы переместить этот код в другое место. - person Roman Starkov; 29.11.2009
comment
Это не проблема. Код удаления вызывается в обоих случаях, но в коде catch вы можете выполнить код extra перед вызовом удаления. Например, вы можете установить какое-то логическое значение внутри x, которое можно проверить при вызове Dispose. - person Mark Byers; 29.11.2009
comment
Спасибо за ваши усилия, но если нам все же нужно разместить обычное предложение try / catch, тогда в этом нет никакого смысла - как вы правильно заметили, материал, который входит в if (ErrorOccurred)/else, должен просто входить в _2 _ / _ 3_. Идея состоит в том, чтобы сохранить повторяющиеся попытки / catch / finally с using, потому что это сократит повсюду шаблонный код. - person Roman Starkov; 29.11.2009
comment
Я обновил свой комментарий, см. Его конец (он очень длинный). Больше нет блока try catch, только дополнительный оператор фиксации. - person Mark Byers; 29.11.2009

Это не кажется такой уж плохой идеей; это просто не кажется идеальным в C # /. NET.

В C ++ есть функция, которая позволяет коду определять, вызван ли он из-за исключения. Это наиболее важно в деструкторах RAII; деструктор может выбрать фиксацию или прерывание в зависимости от того, является ли поток управления нормальным или исключительным. Я думаю, что это довольно естественный подход, но отсутствие встроенной поддержки (и морально сомнительная природа обходного пути; мне кажется, что это скорее зависит от реализации), вероятно, означает, что следует использовать более традиционный подход.

person DrPizza    schedule 29.11.2009