Определение нормального / исключительного выхода из блока using в .net

#c# #.net

#c# #.net

Вопрос:

Возможно ли определить разницу между обычным выходом using блока и выходом, вызванным возникновением исключения? Используя try / catch /, наконец, я могу написать:

 Foo f = new Foo();
try 
{
    f.stuff();
} 
catch() 
{
    f.ThereWasAnError();
} 
finally 
{
    f.Dispose();
}
  

но было бы неплохо, если бы пользователям моего класса не нужно было заботиться об этом, и они могли бы написать

 using (var f = new Foo()) { ... }
  

Я не вижу способа сообщить в методе Dispose, почему меня вызывают. Возможно ли это?

Комментарии:

1. Зачем вам нужно знать? Что вы собираетесь сделать по-другому?

2. using( var f = newFoo()) { ... f.commit();} с Dispose блоком, вероятно, сработало бы при условии, что пользователь вызывает commit себя. Но я думаю, что это неправильный способ сделать это.

Ответ №1:

Было бы полезно, если бы вы объяснили, что вы пытаетесь сделать. Я подозреваю, что вы пытаетесь сделать что-то вроде этого:

 using(t = new Transaction())
{
    DoStuff(t);
}
  

где выполняется транзакция.Dispose(), который вы хотите написать, выглядит следующим образом:

 void Dispose()
{
    if (I'm being disposed because of an exception)
        Roll back the transaction
    else
        commit the transaction
}
  

Это злоупотребление одноразовым шаблоном. Цель одноразового шаблона — вежливо освободить неуправляемые ресурсы как можно скорее независимо от того, почему объект удаляется. Это не для реализации семантики транзакции. Если вы хотите иметь семантику транзакции, вам придется написать код самостоятельно; этот шаблон не встроен в язык C #. Способ написать это так:

 Transaction t = new Transaction();
try
{
    DoStuff(t);

}
catch
{
    t.Rollback();
    throw;
}
t.Commit();
  

(Конечно, имея в виду, что может возникнуть исключение прерывания потока, вызванное до фиксации, но после doStuff; если это вас беспокоит, тогда вам нужно будет написать код с ограниченным регионом выполнения.)

Если вы хотите упростить задачу для своих пользователей, инкапсулируйте это в вспомогательный метод более высокого порядка:

 static void WithTransaction(Action<Transaction> action)
{
    Transaction t = new Transaction();
    try
    {
        action(t);
    }
    catch
    {
        t.Rollback();
        throw;
    }
    t.Commit();
}
....
WithTransaction(t=>DoStuff(t));
  

Комментарии:

1. Как насчет использования RuntimeHelpers.ExecuteCodeWithGuaranteedCleanup Method ? Я видел, как это упоминалось на странице с ограниченными областями выполнения .

2. @Brian: Верно, в принципе, это то, что я здесь набрасываю. Обычно этот метод используется только авторами компиляторов, которые генерируют код для языков, которые поддерживают транзакции как основную концепцию языка, но я полагаю, вы могли бы использовать его сами, если бы захотели.

3. Я полагаю, что это злоупотребление блоком using. Класс Foo будет определять время выполнения блока кода и регистрировать, если он вызвал исключение. С одной стороны, это было бы злоупотреблением языковой функцией, с другой стороны, это затруднило бы неправильное использование API.

Ответ №2:

Если Dispose() требуется знать, была ли ошибка, или ее нужно вызывать, только если все прошло нормально, то это плохая Dispose реализация.

Dispose речь идет не об обработке ошибок, а о том, чтобы гарантировать, что при любых условиях ваш неуправляемый ресурс будет освобожден.

Вы бы выполнили обработку ошибок как обычно, но поместили using блок внутри try блока:

 try
{
   using(...)
   {
      ...
   }
}
catch(Exception e)
{
   ...
}
  

Ответ №3:

Возможно ли определить разницу между обычным выходом блока using() и […] генерируемым исключением?

Это не цель блока using, поэтому: Нет.

 catch() 
{
  f.ThereWasAnError();
}
  

Если вам действительно нужна какая-то форма обработки ошибок внутри ваших объектов, тогда встроите ее. Это не должно нуждаться в посторонней помощи. Это, вероятно, потребовало бы try / catch в каждом общедоступном методе, но такова цена.

Но я бы с большим подозрением отнесся к такому дизайну.

Ответ №4:

Я не рекомендую следующее. Как пишет Эрик Липперт, это злоупотребление одноразовым шаблоном.

При этом должно сработать следующее:

 public void Dispose(){
    if(Marshal.GetExceptionCode()==0){
        // disposing normally
        Commit();
    }else{
        // disposing because of exception.
        Rollback();
    }
}
  

Ответ №5:

Насколько я знаю, нет, но на первый взгляд я бы пропустил блок catch и позволил всплывающей ошибке всплыть. YMMV.

Ответ №6:

Нет никакого способа заставить CLR сообщить вашему Dispose методу, что вот-вот будет выдано исключение.

Но странная вещь заключается в том, что вы хотите «уведомить» свой собственный Foo класс об исключении, которое произошло в его собственном Stuff методе. Делать это очень необычно; вы когда-нибудь видели класс, который вам нужно уведомить о том, что он только что выдал исключение?

Вы должны использовать IDisposable шаблон для выполнения некоторой неуправляемой очистки после того, как ваш класс больше не нужен. Исходя из вашего вопроса, я подозреваю, что ваш класс выполняет множество реальных функций внутри Dispose метода.

Ответ №7:

используйте try{}catch{} внутри блока using().

Ответ №8:

Что ж. Если при удалении вас интересует только то, было ли вызвано исключение удаляемым экземпляром, вы могли бы кэшировать исключение:

 class Foo
{
    Exception _thrownException;
    void Stuff()
    {
        try
        {
            //...
        }
        catch (Exception e)
        {
            _thrownException = e;
            throw;
        }
  //... I suppose you get the idea from here.
  

Однако это не позволит вам узнать, вызвало ли исключение что-то другое в блоке using, например:

 using (var f = new Foo())
{
    int i = f.IntegerMethod() / 0;
    ...
}
  

Ответ №9:

Я выполнил

 using(Foo f = new Foo())
{     
  f.stuff(); 
  f.IsOk();
}  
  

Затем метод dispose знает, был ли он вызван из-за исключения или просто скрыл конец блока «try» или «using».

Иногда «isOk()» называется «Commit()»…