Feel Good.

16 февраля 2010

Об Exception в блоке using

Работать с исключениями всегда надо предельно внимательно, особенно если его следует ожидать внутри блока using.

Представим себе ситуацию: у нас есть класс Some, реализующий интерфейс IDisposable следующим образом:

class Some : IDisposable

{

    public void Dispose()

    {

        throw new Exception("Исключение в Some.Dispose()");

    }

}


Для объектов, реализующих интерфейс IDisposable, мы можем написать следующую конструкцию, используя конструкцию using:

using (Some some = new Some())

{

    //...

    throw new Exception("Очень важный текст ошибки");

}


Отметим, что исключение возникает в двух местах: в блоке using и в методе some.Dispose().

Вопрос: Какое исключение мы поймаем за пределами блока using: исключение, возникшее внутри блока using или исключение возникшее в методе some.Dispose() ?

Ответ: Исключение в методе some.Dispose(). Так как some.Dispose() вызовется немедленно после того, как мы покинем блок using при первом возникновении исключения. Важно отметить, что при этом мы теряем информацию об исключении внутри блока using.

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

Some some = new Some();

try

{

    // используя объект some получаем исключение:

    throw new Exception("Очень важный текст ошибки");

}

finally

{

    IDisposable disposable = some as IDisposable;

    if (disposable != null)

    {

        try

        {

            disposable.Dispose();

        }

        catch (Exception innerEx)

        {

            // обрабатываем, второстепенное исключение innerEx

        }

    }

}



Например, подобная проблема может возникнуть если Вы работаете с экземпляром WCF сервиса, где метод Dispose() сервиса неявно вызывает метод Close(), который в некоторых ситуациях выкидывает исключение.

См. также:
Avoiding Problems with the Using Statement

UPD: Спасибо Sergey Litvinov за ценный комментарий, действительно, если класс Some содержит Finalize метод, который может выбросить исключение, а использование экземпляра Some происходит без конструкции using или явного вызова Dispose метода, то в этом случае, исключение будет поймано уже в GC. Например, при вызове метода GC.Collect().

class Some : IDisposable

{

    ~Some()

    {

        Dispose(false);

    }           

 

    public void Dispose()

    {

        Dispose(true);

        GC.SuppressFinalize(this);

    }

 

    protected virtual void Dispose(bool disposing)

    {

        if (disposing)

        {

            // освободим управляемые ресурсы

        }

        // освободим неуправляемые ресурсы               

        throw new Exception("Dispose(bool)");

    }

}



UPD: Спасибо _FRED_ за полезную ссылку на статью, в которой предлагается идея написать свой safe IDisposable wrapper, общая идея которого заключается в том, чтобы обернуть вызов метода Dispose:

class SafeDisposeWrapper<T> : IDisposable

    where T : IDisposable

{

    public T Disposable { get; set; }

 

    public virtual void Dispose()

    {

        try

        {

            if (Disposable != null)

                Disposable.Dispose();

        }

        catch (Exception ex)

        {

            // обрабатываем исключение

        }

        finally

        {

            GC.SuppressFinalize(this);

        }

    }

 

    public SafeDisposeWrapper(T disposable)

    {

        Disposable = disposable;

    }

}


Использовать такой Wrapper достаточно просто:

Some some = new Some();

using (new SafeDisposeWrapper<Some>(some))

{

    // работаем с some...

}


Как вариант, который предложил zhe в комментариях, можно использовать обработчики исключений, передавая их в конструктор Wrapper'а.

UPD:
Интересная ссылка Digging into IDisposable

12 комментариев:

  1. А в каких случаях надо самому вызывать метод Dispose(); ?

    ОтветитьУдалить
  2. Например, если Вы хотите принудительно освободить ресурсы, но лучше использовать конструкцию using.

    ОтветитьУдалить
  3. Так же нужно учесть, если класс Some будет использован без using-а, и он вызывает using в ~Some, то exception будет отловлен уже GC.

    ОтветитьУдалить
  4. Для WCF прокси нужно использовать немного другой подход:
    http://msdn.microsoft.com/en-us/library/aa355056.aspx

    ОтветитьУдалить
  5. Есть очень простое правило на счёт исключений в IDisposable. Правило описано в МСДН к методу Dispose:

    If an object's Dispose method is called more than once, the object must ignore all calls after the first one. The object must not throw an exception if its Dispose method is called multiple times.

    То есть Dispose() должен быть к тому, что его вызовут несколько раз и не должен бросать исключений.

    Поэтому рассуждения о том, что же будет (а тем более решения, расходящиеся с прямой настоятельной рекомендацией), если исключение случится, кажутся по меньшей мере очень странными.

    ОтветитьУдалить
  6. Вы не правильно поняли, статья немного о том.
    Это правило, которое Вы привели из MSDN, определяет, что при повторном(multiple times) вызове метода Dispose никаких исключений не должно быть. При этом предполагалось, что Dispose при первом и последующих вызовах отработает корректно.

    ОтветитьУдалить
  7. Ага, я невнимательно вчитался. Ну что поделаешь: писатели сами себе бяки :о)

    За них то, что нужно сказал Джо (http://www.bluebytesoftware.com/blog/PermaLink.aspx?guid=88e62cdf-5919-4ac7-bc33-20c06ae539ae, поиск по "Avoid throwing an exception from within Dispose")

    Оговорка про "critical situations" там смотрится очень странно, так как в таких ситуациях ("leaks, inconsistent shared state, etc.") впору завершать процесс, ибо гарантировать корректную работу после такого сбоя проблематично.

    Приведённый пример с WCF больше напоминает недоработку в дизайне, и бороться надо с недоработкой. Например, можно или не реализовывать IDisposable, если не можешь реализовать его корректно или, если сделать Dispose() пустым, обязав коллера вызывать явный Close() и ловить исключения.

    Думать о том, как бороться с исключениями, брошенными файнализатором ещё более бессмысленно: без спец. настроек приложение завершится.

    Собственно, я всё о том, что лучше пропагандировать то, почему из файнализаторов, Dispose() и finally нельзя бросать исключения, а не то, как с этим можно жить.

    ОтветитьУдалить
  8. Кстати, вот ещё немного собранной вместе информации по данному вопросу и один из способов решения той же задачи: http://blog.hypercomplex.co.uk/index.php/2009/11/should-dispose-throw-exceptions/

    ОтветитьУдалить
  9. @_FRED_
    Спасибо за ссылку, фактически моя статья описывает так называемый "Exception masking".
    Кстасти, было интересно прочитать про "safe IDisposable wrapper": http://marcgravell.blogspot.com/2008/11/dontdontuse-using.html

    ОтветитьУдалить
  10. Лично мне сразу пришла в голову идея наваять вот такой враппер на такие случаи.

    public class SafeDisposeWrapper : IDisposable
    where T : IDisposable
    {
    public SafeDisposeWrapper(T @object, Action destructorExHandler)
    {
    Object = @object;
    _destructorExHandler = destructorExHandler;
    }

    public T Object { get; private set; }

    public void Dispose()
    {
    try
    {
    Object.Dispose();
    }
    catch (Exception e)
    {
    if (_destructorExHandler != null)
    _destructorExHandler(e);
    }
    finally
    {
    GC.SuppressFinalize(this);
    }
    }

    private readonly Action _destructorExHandler;
    }


    Используется как-то так:

    Some some;
    using (new SafeDisposeWrapper(some = new Some(),
    e => Console.WriteLine("Ololo!!! Teh destructor exception: {0}", e.ToString())))
    {
    try
    {
    some.SomeMethod();

    throw new Exception();
    }
    catch (Exception e)
    {
    // тут хендлим наш важный ексепшн
    }
    }



    ПС. оказывается, в предыдущем коменте это уже было. Зря старался :)

    ОтветитьУдалить
  11. Странно, при коменте съелись определения генериков < T >
    Но я думаю, суть и так ясна.

    ОтветитьУдалить
  12. Всем спасибо за комментарии, добавил два обновления в статью.

    ОтветитьУдалить