Сенс розпорядження - звільнити некеровані ресурси. Це потрібно зробити в якийсь момент, інакше вони ніколи не будуть прибиратися. Збирач сміття не знає, як викликати DeleteHandle()
змінну типу IntPtr
, він не знає, чи потрібно йому дзвонити DeleteHandle()
.
Примітка . Що таке некерований ресурс ? Якщо ви знайшли його в Microsoft .NET Framework: він керується. Якщо ви самі ходили колупати навколо MSDN, це некеровано. Все, що ви використовували P / Invoke дзвінки, щоб вийти за межі приємного світу зручності всього, що вам доступне в .NET Framework, не керовано - і тепер ви відповідаєте за його очищення.
Об'єкт, який ви створили, повинен розкрити якийсь метод, який може викликати зовнішній світ, щоб очистити некеровані ресурси. Метод можна назвати все, що завгодно:
public void Cleanup()
або
public void Shutdown()
Але замість цього є стандартизована назва цього методу:
public void Dispose()
Був навіть створений інтерфейс IDisposable
, у якому є лише один метод:
public interface IDisposable
{
void Dispose()
}
Таким чином, ви змушуєте ваш об'єкт відкривати IDisposable
інтерфейс, і таким чином ви обіцяєте, що ви написали цей єдиний метод для очищення ваших некерованих ресурсів:
public void Dispose()
{
Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);
}
І ви закінчили. За винятком, що ви можете зробити краще.
Що робити, якщо ваш об'єкт виділив 250MB System.Drawing.Bitmap (тобто клас. Bitmap, керований .NET) як якийсь буфер кадру? Звичайно, це керований об’єкт .NET, і збирач сміття звільнить його. Але чи справді ви хочете залишити 250 Мб пам'яті, просто сидячи там - чекаючи, коли збирач сміття згодом прийде і звільнить його? Що робити, якщо є відкрите підключення до бази даних ? Звичайно, ми не хочемо, щоб це з'єднання було відкритим, чекаючи, коли GC доопрацює об'єкт.
Якщо користувач подзвонив Dispose()
(це означає, що вони більше не планують використовувати об'єкт), чому б не позбутися цих марнотратних растрових зображень та підключень до бази даних?
Тож тепер ми:
- позбутися некерованих ресурсів (тому що ми маємо) та
- позбутися керованих ресурсів (тому що ми хочемо бути корисними)
Тож давайте оновимо наш Dispose()
метод, щоб позбутися цих керованих об'єктів:
public void Dispose()
{
//Free unmanaged resources
Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);
//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()
методу:
~MyObject()
{
//we're being finalized (i.e. destroyed), call Dispose in case the user forgot to
Dispose(); //<--Warning: subtle bug! Keep reading!
}
Але в цьому коді є помилка. Розумієте, збирач сміття працює по фоновій нитці ; ви не знаєте, в якому порядку знищуються два об'єкти. Цілком можливо, що у вашому Dispose()
коді керованого об’єкта, від якого ви намагаєтеся позбутися (оскільки ви хотіли бути корисним), вже немає:
public void Dispose()
{
//Free unmanaged resources
Win32.DestroyHandle(this.gdiCursorBitmapStreamFileHandle);
//Free managed resources too
if (this.databaseConnection != null)
{
this.databaseConnection.Dispose(); //<-- crash, GC already destroyed it
this.databaseConnection = null;
}
if (this.frameBufferImage != null)
{
this.frameBufferImage.Dispose(); //<-- crash, GC already destroyed it
this.frameBufferImage = null;
}
}
Отже, що вам потрібно - це спосіб Finalize()
розповістиDispose()
що він не повинен торкатися жодних керованих ресурсів (бо їх може не бути там) ), при цьому вивільняючи некеровані ресурси.
Стандартний зразок для цього - мати Finalize()
і Dispose()
обидва виклику a третій (!) Метод; де ви передаєте булеву приказку, якщо ви її телефонуєте Dispose()
(на відміну від Finalize()
), це означає, що це безпечно для безкоштовних керованих ресурсів.
Цей внутрішній метод міг би бути даний якоюсь довільною назвою на кшталт "CoreDispose" або "MyInternalDispose", але традиція його називати Dispose(Boolean)
:
protected void Dispose(Boolean disposing)
Але більш корисною назвою параметра може бути:
protected void Dispose(Boolean itIsSafeToAlsoFreeManagedObjects)
{
//Free unmanaged resources
Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);
//Free managed resources too, but only if I'm being called from Dispose
//(If I'm being called from Finalize then the objects might not exist
//anymore
if (itIsSafeToAlsoFreeManagedObjects)
{
if (this.databaseConnection != null)
{
this.databaseConnection.Dispose();
this.databaseConnection = null;
}
if (this.frameBufferImage != null)
{
this.frameBufferImage.Dispose();
this.frameBufferImage = null;
}
}
}
І ви змінюєте свою реалізацію IDisposable.Dispose()
методу на:
public void Dispose()
{
Dispose(true); //I am calling you from Dispose, it's safe
}
і ваш фіналізатор для:
~MyObject()
{
Dispose(false); //I am *not* calling you from Dispose, it's *not* safe
}
Примітка : Якщо ваш об'єкт походить від об'єкта, який реалізує Dispose
, тоді не забудьте зателефонувати до їх базового методу розпорядження, коли ви перекриєте Dispose:
public override void Dispose()
{
try
{
Dispose(true); //true: safe to free managed resources
}
finally
{
base.Dispose();
}
}
І все добре, крім того, що ти можеш зробити краще !
Якщо користувач зателефонував Dispose()
на ваш об’єкт, то все очищено. Пізніше, коли збирач сміття підійде і зателефонує Finalize, він знову зателефонує Dispose
.
Це не тільки марно, але якщо ваш об’єкт має непотрібні посилання на об’єкти, які ви вже утилізували останнього дзвінка Dispose()
, ви спробуйте їх розпоряджатись знову!
Ви помітите в моєму коді, що я обережно видаляв посилання на об’єкти, які я розмістив, тому не намагаюся телефонувати Dispose
на непотрібний об'єкт. Але це не завадило тонкому клопу повзати.
Коли користувач викликає Dispose()
: ручка CursorFileBitmapIconServiceHandle знищена. Пізніше, коли працює сміттєзбірник, він знову спробує знищити ту саму ручку.
protected void Dispose(Boolean iAmBeingCalledFromDisposeAndNotFinalize)
{
//Free unmanaged resources
Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle); //<--double destroy
...
}
Як ви це вирішите, повідомте сміттєзбірнику, що йому не потрібно заважати доопрацьовувати об’єкт - його ресурси вже очищені, і більше роботи не потрібно. Ви робите це, зателефонувавши GC.SuppressFinalize()
в Dispose()
метод:
public void Dispose()
{
Dispose(true); //I am calling you from Dispose, it's safe
GC.SuppressFinalize(this); //Hey, GC: don't bother calling finalize later
}
Тепер, коли користувач зателефонував Dispose()
, ми маємо:
- звільнили некеровані ресурси
- звільнили керовані ресурси
В GC немає сенсу запускати фіналізатор - про все подбали.
Не можу я використовувати Finalize для очищення некерованих ресурсів?
Документація для Object.Finalize
:
Метод Finalize використовується для виконання операцій очищення на некерованих ресурсах, що зберігаються поточним об'єктом до знищення об'єкта.
Але документація MSDN також говорить про IDisposable.Dispose
:
Виконує завдання, визначені додатком, пов’язані із звільненням, звільненням або скиданням некерованих ресурсів.
Так що це? Яке з них є місцем для очищення некерованих ресурсів? Відповідь:
Це твій вибір! Але вибирай Dispose
.
Ви, безумовно, можете розмістити свою некеровану очистку у фіналізаторі:
~MyObject()
{
//Free unmanaged resources
Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);
//A C# destructor automatically calls the destructor of its base class.
}
Проблема в тому, що ви не знаєте, коли збирач сміття обійдеться, щоб доопрацювати ваш об’єкт. Ваші некеровані, непотрібні, не використовувані рідні ресурси будуть триматися, поки сміттєзбірник врешті не запуститься. Тоді він зателефонує до вашого методу завершення; прибирання некерованих ресурсів. Документація Object.Finalize вказує на це:
Не визначений точний час, коли виконується фіналізатор. Щоб забезпечити детермінований випуск ресурсів для екземплярів вашого класу, застосуйте метод Close або забезпечте IDisposable.Dispose
реалізацію.
Це чеснота використання Dispose
для очищення некерованих ресурсів; ви дізнаєтесь і контролюєте, коли очищений некерований ресурс. Їх знищення є "детермінованим" .
Щоб відповісти на своє первісне запитання: чому б не звільнити пам'ять зараз, а не тоді, коли GC вирішить це зробити? У мене є особи програмного забезпечення для розпізнавання , що потреби , щоб позбутися від 530 МБ внутрішніх образів в даний час , так як вони більше не потрібно. Коли ми цього не робимо: машина перемелюється до місця обміну.
Бонусне читання
Для всіх, кому подобається стиль цієї відповіді (пояснюючи чому , так як стає очевидним), пропоную прочитати першу главу Основного COM від Дон Бокса:
На 35 сторінках він пояснює проблеми використання бінарних об'єктів і вигадує COM перед вашими очима. Після того, як ви зрозумієте, чому саме COM, залишилися 300 сторінок очевидні, і лише детально описано впровадження Microsoft.
Я думаю, що кожен програміст, який коли-небудь мав справу з об'єктами чи COM, повинен, принаймні, прочитати першу главу. Це найкраще пояснення чого-небудь коли-небудь.
Додаткове бонусне читання
Коли Ерік Ліпперт усе, що ви знаєте, не так
Тому справді дуже складно написати правильний фіналізатор, і найкраща порада, яку я можу дати вам, - це не намагатися .