Feel Good.

22 июля 2010

Раcширяем IDisposable

Наверняка Вы сталкивались с ситуацией, когда Вам требуется совершить явный вызов метода Dispose у объекта, реализующего интерфейс IDisposable, при этом, для каждого такого Dispose-вызова приходиться постоянно писать одинаковый код, по следующему шаблону (проверка на null, вызов IDisposable.Dispose):

// Освобождение ресурсов

if (some != null)

{

    some.Dispose();

}



Раз этот код сильно похож и выделяется в шаблон, то его логично вынести в отдельный метод, а еще лучше в Extension Method для типа IDisposable:

static class IDisposableExtention

{

    // Безопасный, относительно NullReferenceException Dispose

    public static void SafeDispose(this IDisposable self)

    {

        if (self != null)

        {

            self.Dispose();

        }

    }

}


После чего, искомые четыре строчки кода для Dispose-вызова свернуться до одной - вызова нашего метода SafeDispose:

some.SafeDispose();

some.SafeDispose(); // При повторном вызове ничего не произойдет.



Progg it

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

  1. Присвоение self = null тут лишнее. Ведь фактически вы обнуляете ссылку на экземпляр лишь в локальной переменной-параметре self.

    То есть после вызова some.SafeDispose(); переменная some не станет равной null.

    ОтветитьУдалить
  2. Так ведь всё равно some не станет равной null.

    ОтветитьУдалить
  3. Да, занулить some тогда никак не получиться, впринципе, можно и без зануления обойтись (обычный Dispose).
    Может есть у кого идеи, как еще и занулять some?

    ОтветитьУдалить
  4. Как-то так.

    Можно использовать класс-хелпер, в единственный метод которого можно передавать переменную по ссылке:

    static class DisposeHelper
    {
    public static void SafeDisposeAndNull(ref object instance)
    {
    var disposable = instance as IDisposable;
    if (disposable != null)
    {
    disposable.Dispose();
    }

    instance = null;
    }
    }

    использование:

    DisposeHelper.SafeDispose(some);

    Но тут теряется сама идея расширения за счёт extension-методов.

    ОтветитьУдалить
  5. Упс. Накосячил с использованием. :) Должно быть так.

    DisposeHelper.SafeDispose(ref some);

    ОтветитьУдалить
  6. Опять накосячил ))))) Должно быть так:

    DisposeHelper.SafeDisposeAndNull(ref some);

    ОтветитьУдалить
  7. Что-то я не пойму, зачем занулять some?
    Ведь после того как мы вызвали Dispose, это уже не мы заботимся о переменной, а сборщик мусора.

    По идее для нас Dispose фактически тоже самое, что и уничтожение объекта, и далее его использовать нельзя.

    ОтветитьУдалить
  8. 2Vadim Sentyaev:
    теоретически возможна ситуация, когда вы сможете попытаться вызвать метод уже диспоузнутого экземпляра. И этот метод может даже успешно выполниться :)
    Видимо автор занулением some пытается избежать таких ситуаций и сделать код более робустным ))

    Лучше, если вызов такого метода упадёт по NullReferenceException, нежели выполнится.

    ОтветитьУдалить
  9. @zhe
    В идеале конечно же надо ловить ObjectDisposedException, но когда объект null , все сомнения в том, что объект еще можно использовать отпадают).

    @Vadim Sentyaev
    Но все-таки главная идея избавиться от проверки на null.

    В качестве бонуса можно добавить свой код pre/post код перед/после Dispose:
    public static void MyDispose(this IDisposable self)
    {
    if (self != null)
    {
    // pre-code
    self.Dispose();
    // post-code
    }
    }

    ОтветитьУдалить
  10. zhe:
    Это да.
    С другой стороны есть
    using (Some some = new Some()) {}
    а далее к нему уже не обратишься.

    ОтветитьУдалить
  11. Vadim Sentyaev, согласен. С using вообще не надо городить весь огород с проверками, обнулениями и т. д.

    Автор видать имеет в виду примеры, когда using использовать низя. Например, если создание объекта происходит в одном месте, а его уничтожение - вообще хрен знает где :)
    Бывают и такие ситуации.

    ОтветитьУдалить
  12. Бывает даже так:
    Some some;
    using (some = new Some()) { ... }

    ОтветитьУдалить
  13. Или даже так :)

    Some some = new Some();
    using (some) { ... }

    ОтветитьУдалить
  14. Походу у меня знаний не хватает...

    using (Some some = new Some()) {}
    разворачивается на:
    Some some = new Some()
    try
    {
    ...
    }
    finally
    {
    ((IDisposable)some).Dispose();
    }

    Тогда в зачем использовать
    Some some;
    using (some = new Some()) { ... }
    или
    Some some = new Some();
    using (some) { ... }

    Ведь в любом случае после блока using к переменным нельзя обращаться, они уже как-бы "уничтожены", ну или если это применить к Stream, то он может быть закрыть.

    ОтветитьУдалить
  15. Vadim Sentyaev, рассуждаете правильно. Но язык разрешает это делать. Ничего не поделаешь )))

    ОтветитьУдалить
  16. Я читал на codeproject статейку, в которой индийский программист призывал всех создавать строки DataRow примерно таким способом:
    using(DataRow row = table.NewRow()){
    //тут строка заполняется значениями
    table.Rows.Add(row);
    }
    потом идет работа с таблицей...
    И ведь работает, к сожалению... А тот программист был уверен что это очень наглядный пример использования IDisposable

    ОтветитьУдалить
  17. Почему называется safe?
    Thread safe диспоузинга вроде нет.

    ОтветитьУдалить
  18. @Omari
    Safe относительно NullReferenceException.
    Например, если some=null, то some.SafeDispose(); отработает нормально, и проверка на null не нужна.

    ОтветитьУдалить
  19. Насколько я помню, под MDX еще имеет смысл проверять IsDisposed или то-то вроже этого.

    ОтветитьУдалить
  20. Мне кажется, что присвоить ссылке null после вызова Dispose - это хороший тон.

    ОтветитьУдалить
  21. Что мешает писать:

    using (some) {}

    в качестве безопасного Dispose?

    ОтветитьУдалить
  22. @zhe
    вариант с ref object работать не будет.
    можно так:
    static public void SafeDisposeAndNull(ref T self) where T : class, IDisposable {
    if ( null != self ) {
    self.Dispose();
    self = null;
    }
    }
    ;)

    ОтветитьУдалить
  23. @Илья Дубаденко
    Мне кажется, что присвоить ссылке null после вызова Dispose - это хороший тон.

    Мне почему то думается что не совсем это хорошая идея.

    Давайте посмотрим на пару IDisposable классов.
    1. Stream
    После Dispose можно проверить CanRead, CanWrite
    2. SqlConnection
    После Dispose можно проверить ConnectionState.
    Причем соединение может закрыться и до вызова Dispose. Так что в любом случае нужно обрабатывать ошибки.
    3. SerialPort
    После Dispose можно проверить IsOpen
    И порт может независимо от нас закрыться. Например зависнит подключеный к COM порту GSM модем (стандартная ситуация).


    Ну и такое.
    Например если мы реализуем пул соединений, то вызов Dispose() на конкретном соединении из пула просто вернет его в пул.

    Мне кажется за Dispose нужно программисту следить.
    А то получается мы хотим сдлать деструктор.

    В итоге на мой взгляд лучший вариант, если уж очень хочется занулить объект после Dispose это:
    some.SafeDispose();
    some = null;

    ОтветитьУдалить
  24. dogwatch, это почему это мой вариант не будет работать?

    ОтветитьУдалить
  25. dogwatch, "if ( null != self )" - Йода-код детектед )))

    ОтветитьУдалить
  26. "Мне кажется, что присвоить ссылке null после вызова Dispose - это хороший тон"
    По моему ясней, когда это явно делает объект - владелец ссылки. К тому же он еще и отвечает за время жизни объекта.
    А так получается, что ссылка вдруг обнулилась и слово "Safe" не вносит ясности. Может "DisposeAndNullify"?

    ОтветитьУдалить
  27. @zhe
    тип должен совпадать. иначе ошибка компиляции

    ОтветитьУдалить
  28. @Omari
    Совершенно с Вам согласен с тем, что занулением должен заниматься владелец объекта. Вот только хотелось бы придумать красивый путь зануления через extention-метод (похоже такого решения не существует). Но пока занулять можно явно присвоить null или через Helper-метод.

    ОтветитьУдалить
  29. 2dogwatch:

    Вставил свой код в студию, компилируется и работает нормально. Что я делаю не так?

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