Форум программистов, компьютерный форум, киберфорум
Exerion
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск  

IDisposable - правильное использование

Запись от Exerion размещена 16.06.2015 в 15:53
Показов 489 Комментарии 0
Метки c#, dispose, finalize, gc, idisposable

Довольно долгое время я мучился в попытках познать тонкости интерфейса IDisposable и метода Dispose, особенно вводило меня в ступор наличие перегрузки этого метода с булевым аргументом. Со временем те или иные аспекты становились яснее, но полной картины у меня в голове не было до тех пор, пока я не наткнулся на ответ/статью по данной теме на stackoverflow.com, мой вольный перевод которой вы можете увидеть ниже.


Смысл Dispose - освободить неуправляемые ресурсы (прим.переводчика - в качестве неуправляемых ресурсов можно привести соединения с базами данных, сокеты, дескрипторы окон и т.д). Освобождение неуправляемых ресурсов должно быть произведено в определённый момент, иначе они не будут освобождены вообще. Сборщик мусора (garbage collector, GC) не знает, как вызывать метод DeleteHandle() на переменной типа IntPtr, а также не знает, нужно ли вообще его вызывать.
Прим.: Что такое неуправляемые ресурсы? Если вы пользуетесь чем-либо внутри Microsoft .NET Framework - это управляемые ресурсы. Если вы решили полазать по MSDN самостоятельно, это неуправляемые ресурсы. Всё, что используется, когда вы вызываете P/Invoke чтобы выбраться за границы уютненького мира возможностей .NET Framework - неуправляемые ресурсы, и вы теперь ответственны за их очистку.
Создаваемый вами объект должен иметь метод, который мог бы быть вызван внешним миром для очистки неуправляемых ресурсов. Существует даже стандартизированное название для этого метода:
C#
1
public void Dispose()
Мало того, даже был создан интерфейс - IDisposable - который имеет всего один этот метод:
C#
1
2
3
4
public interface IDisposable
{
   void Dispose()
}
Итак, в своём объекте вы реализуете интерфейс IDisposable и таким образом даёте обещание, что вы написали этот единственный метод для очистки неуправляемых ресурсов:
C#
1
2
3
4
public void Dispose()
{
   Win32.DestroyHandle(this.gdiCursorBitmapStreamFileHandle);
}
Вот и всё. Однако можно сделать лучше.
________________________________________ ________________________________________ ___

Что, если ваш объект выделил 250Мб System.Drawing.Bitmap (т.е. управляемый .NET Bitmap класс) в качестве некоего кадрового буфера? Безусловно, это управляемый объект, и сборщик мусора освободит память, занимаемую им. Но действительно ли вы хотите оставить кусок памяти в 250Мб просто лежать где-то, дожидаясь когда сборщик мусора удосужится добраться и освободить его? А что, если там же есть открытое соединение с базой данных? Однозначно мы не хотели бы оставлять это соединение открытым, ожидая пока GC финализирует объект.

Поэтому мы:
  • разберёмся с неуправляемыми ресурсами (потому что мы должны), и
  • разберёмся с управляемыми ресурсами (потому что мы хотим быть полезными)

Итак, давайте обновим наш Dispose() метод, чтобы избавиться от управляемых ресурсов:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void Dispose()
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.gdiCursorBitmapStreamFileHandle);
 
   //Free managed resources too
   if (this.databaseConnection != null)
   {
      this.databaseConnection.Dispose();
      this.databaseConnection = null;
   }
   if (this.frameBufferImage != null)
   {
      this.frameBufferImage.Dispose();
      this.frameBufferImage = null;
   }
}
Всё это здорово, однако можно сделать лучше!
________________________________________ ________________________________________ ___

А что, если человек забыл вызывать Dispose() на вашем объекте? Произойдёт утечка неуправляемых ресурсов!
Прим.: Утечки управляемых ресурсов не будет, потому что сборщик мусора рано или поздно запустится в фоновом потоке и очистит память, занимаемую любыми неиспользуемыми объектами. Сюда входит ваш объект и любые используемые вами управляемые объекты (те же Bitmap и DbConnection).
Если человек забыл вызвать Dispose(), мы всё ещё можем спасти ситуацию. Мы можем вызывать этот метод, когда сборщик мусора доберётся до объекта, чтобы освободить (финализировать) его.
Прим.: Сборщик мусора рано или поздно освобождает все управляемые объекты путём вызова метода Finalize на объекте. GC ничего не знает о вашем Dispose методе и ему нет до него никакого дела. Это всего лишь название, которое мы выбрали для метода, вызываемого нами для очистки неуправляемых ресурсов.
Уничтожение объекта сборщиком мусора - идеальный момент для освобождения этих надоедливых неуправляемых ресурсов. Мы сделаем это, переопределив метод Finalize().
Прим.: В C# вы не переопределяете метод Finalize() явно. Вместо этого вы пишете метод, который похож на C++ деструктор, а компилятор принимает его как вашу реализацию метода Finalize():
C#
1
2
3
4
5
~MyObject()
{
    //мы были финализированы (т.е. уничтожены), вызовите Dispose() на случай, если пользователь забыл сделать это
    Dispose(); //<--ПРЕДУПРЕЖДЕНИЕ! Тут кроется ошибка! Читайте ниже.
}
Однако в этом коде есть ошибка. Сборщик мусора работает в фоновом потоке, и вы не знаете последовательность, в которой два объекта будут уничтожены. Вполне возможно, что в коде вашего Dispose() метода управляемый объект, от которого вы хотите избавиться, больше не существует:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void Dispose()
{
   //Освободить неуправляемые ресурсы
   Win32.DestroyHandle(this.gdiCursorBitmapStreamFileHandle);
 
   //Освободить также и управляемые ресурсы
   if (this.databaseConnection != null)
   {
      this.databaseConnection.Dispose(); <-- ошибка, GC уже уничтожил это
      this.databaseConnection = null;
   }
   if (this.frameBufferImage != null)
   {
      this.frameBufferImage.Dispose(); <-- ошибка, GC уже уничтожил это
      this.frameBufferImage = null;
   }
}
Что вам нужно, так это способ, с помощью которого метод Finalize() может сказать методу Dispose() не трогать управляемые ресурсы (потому что их может уже и не быть) во время освобождения неуправляемых.
Стандартный паттерн для этого - из обоих методов Finalize() и Dispose() вызывать третий(!) метод, куда вы передадите Boolean значение, означающее, что вы вызываете его из Dispose() (в противоположность Finalize()), в том смысле, что можно безопасно освобождать управляемые ресурсы.

Этот внутренний (internal) метод мог бы иметь произвольное название вроде "CoreDispose" или "MyInternalDispose", но традиционно его называют Dispose(Boolean) (прим.переводчика - откуда и растёт непонимание):
C#
1
protected void Dispose(Boolean disposing)
Но более полезное для понимания имя параметра могло быть таким:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected void Dispose(Boolean itIsSafeToAlsoFreeManagedObjects)
{
   //Освободить неуправляемые ресурсы
   Win32.DestroyHandle(this.gdiCursorBitmapStreamFileHandle);
 
   //Освободить также управляемые ресурсы, но только если вызов был из Dispose
   //(если вызов был из Finalize, эти объекты могут уже не существовать)
   if (itIsSafeToAlsoFreeManagedObjects)  
   {    
      if (this.databaseConnection != null)
      {
         this.databaseConnection.Dispose();
         this.databaseConnection = null;
      }
      if (this.frameBufferImage != null)
      {
         this.frameBufferImage.Dispose();
         this.frameBufferImage = null;
      }
   }
}
Теперь вы меняете свою реализацию метода IDisposable.Dispose() на:
C#
1
2
3
4
public void Dispose()
{
   Dispose(true); //Я вызываю тебя из Dispose(), это безопасно
}
...и реализацию финализатора:
C#
1
2
3
4
~MyObject()
{
   Dispose(false); //Я *НЕ* вызываю тебя из Dispose(), это *НЕ* безопасно
}
Прим.: если ваш объект наследуется от объекта, который реализует Dispose, не забудьте вызывать Dispose базового класса, когда вы переопределяете Dispose:
C#
1
2
3
4
5
6
7
8
9
10
11
public Dispose()
{
    try
    {
        Dispose(true); //true: безопасно для освобождения управляемых ресурсов
    }
    finally
    {
        base.Dispose();
    }
}
Всё это здорово, однако можно сделать лучше!
________________________________________ ________________________________________ ___

Если пользователь вызывает Dispose() на вашем объекте, то всё будет очищено. Позднее, когда сборщик мусора доберётся до объекта и вызовет Finalize, он снова вызовет Dispose.

Не только это расточительно, но и если ваш объект имеет мусорные ссылки на объекты, которые вы уже очистили с прошлого вызова Dispose(), вы вызовете их очистку повторно!

Вы могли заметить в моём коде, что я был аккуратен и удалял ссылки на уже очищенные объекты, таким образом я не вызывал Dispose на "мусорных ссылках". Однако даже это не остановит этот неуловимый баг.

Когда пользователь вызывает Dispose(), дескриптор gdiCursorBitmapStreamFileHandle уничтожается. Позднее, когда отработает сборщик мусора, он попытается уничтожить тот же дескриптор опять.
C#
1
2
3
4
5
6
protected void Dispose(Boolean iAmBeingCalledFromDisposeAndNotFinalize)
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.gdiCursorBitmapStreamFileHandle); <-- двойное уничтожение
   ...
}
Мы можем исправить это, указав сборщику мусора, что ему не нужно беспокоиться о финализации объекта - ресурсы уже очищены и работы больше нет. Это делается путём вызова GC.SuppressFinalize() в методе Dispose():
C#
1
2
3
4
5
public void Dispose()
{
   Dispose(true); //Я вызываю тебя из Dispose(), это безопасно
   GC.SuppressFinalize(this); //Эй, GC. Больше не беспокойся о вызове Finalize
}
Теперь, когда пользователь вызывает Dispose(), мы:
  • очистили неуправляемые ресурсы
  • очистили управляемые ресурсы
Сборщику мусора нет смысла запускать финализатор - мы обо всём уже позаботились.

Текст оригинала на английском: stackoverflow
Метки c#, dispose, finalize, gc, idisposable
Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Был там один разговор по поводу свободы в материальном мире.
kumehtar 19.08.2026
Суть: рассматривается живое существо, оказавшееся внутри довольно странной системы (этого мира) и пытающееся обустроить в ней свой кусок пространства. Жизнь действительно предъявляет каждому. . .
Когда логика программы не спасает от человеческих ошибок
Maks 18.08.2026
В последнее время всё чаще и чаще сталкиваюсь с таким явлением, как абсолютная невнимательность (или глупость) пользователей. Проявляется это чаще всего на работе в коллективе. Допустим, человек с. . .
Лето уходит
kumehtar 17.08.2026
Мысли в слух
kumehtar 17.08.2026
Забавно, насколько сейчас стала доступна информация. Например о магии, духовном развитии, медитациях, и других подобных направлениях, ранее зачастую тайных, передаваемых от учителя к ученику. Хотя. . .
Перемещение строк из ТЧ в другой документ с учетом текущего пробега
Maks 17.08.2026
Реализация из решения ниже выполнена на примере нетипового документа "Автозапчасти", с ТЧ "Шины". За основу взят алгоритм отсюда: https:/ / www. cyberforum. ru/ blogs/ 359708/ 10838. html Задача: . . .
Саморегулирующийся социальный контракт для сервера cross-section.
Hrethgir 14.08.2026
С кодом конечно таких глубоких размышлений пока не было, впрочем я уже привык к алгоритмизации. Суть предмета записи: снова в диалоге с нейросетью (я взял пока себе ник для учётки админа - Rector). . . .
Часы электронные
Uhbif79 12.08.2026
Выкладываю программу часов. Программа позволяет: 1. Использовать системное время и дату, 2. Есть возможность вводить время и дату вручную. 3. Реализованы 2 будильника: начало и конец рабочего дня. . . .
Часы с будильником на основе класса QLCDNumber
Uhbif79 12.08.2026
Всем добрый день, выкладываю программу часов с будильником на основе класса QLCDNumber. Здесь я пробовал самостоятельно создавал классы, впервые столкнулся с видимостью переменной одного класса из. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru