Довольно долгое время я мучился в попытках познать тонкости интерфейса IDisposable и метода Dispose, особенно вводило меня в ступор наличие перегрузки этого метода с булевым аргументом. Со временем те или иные аспекты становились яснее, но полной картины у меня в голове не было до тех пор, пока я не наткнулся на ответ/статью по данной теме на stackoverflow.com, мой вольный перевод которой вы можете увидеть ниже.
Смысл Dispose - освободить неуправляемые ресурсы (прим.переводчика - в качестве неуправляемых ресурсов можно привести соединения с базами данных, сокеты, дескрипторы окон и т.д). Освобождение неуправляемых ресурсов должно быть произведено в определённый момент, иначе они не будут освобождены вообще. Сборщик мусора (garbage collector, GC) не знает, как вызывать метод DeleteHandle() на переменной типа IntPtr, а также не знает, нужно ли вообще его вызывать.
Прим.: Что такое неуправляемые ресурсы? Если вы пользуетесь чем-либо внутри Microsoft .NET Framework - это управляемые ресурсы. Если вы решили полазать по MSDN самостоятельно, это неуправляемые ресурсы. Всё, что используется, когда вы вызываете P/Invoke чтобы выбраться за границы уютненького мира возможностей .NET Framework - неуправляемые ресурсы, и вы теперь ответственны за их очистку.
| Создаваемый вами объект должен иметь метод, который мог бы быть вызван внешним миром для очистки неуправляемых ресурсов. Существует даже стандартизированное название для этого метода:
Мало того, даже был создан интерфейс - 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(), мы:- очистили неуправляемые ресурсы
- очистили управляемые ресурсы
Сборщику мусора нет смысла запускать финализатор - мы обо всём уже позаботились.
|