Установка объекта в null vs Dispose()

Меня увлекает то, как работает CLR и GC (я работаю над расширением моих знаний об этом, читая CLR через С#, книги/сообщения Jon Skeet и т.д.).

В любом случае, в чем разница между высказыванием:

MyClass myclass = new MyClass();
myclass = null;

Или, создав MyClass для реализации IDisposable и деструктора и вызывая Dispose()?

Кроме того, если у меня есть блок кода с оператором using (например, ниже), если я пройду через код и выйду из блока использования, будет ли объект удален или когда произойдет сборка мусора? Что произойдет, если я вызову Dispose() в блоке using any?

using (MyDisposableObj mydispobj = new MyDisposableObj())
{

}

У классов Stream (например, BinaryWriter) есть метод Finalize? Почему я хочу использовать это?

Ответ 1

Важно отделить удаление от сбора мусора. Это совершенно разные вещи, с одной точкой, с которой я приду через минуту.

Dispose, сбор и завершение сборки мусора

Когда вы пишете оператор using, он просто синтаксический сахар для блока try/finally, так что Dispose вызывается, даже если код в теле оператора using вызывает исключение. Это не означает, что объект представляет собой мусор, собранный в конце блока.

Утилизация - это неуправляемые ресурсы (ресурсы без памяти). Это могут быть интерфейсы UI, сетевые подключения, дескрипторы файлов и т.д. Это ограниченные ресурсы, поэтому вы обычно хотите их освободить, как только сможете. Вы должны внедрять IDisposable, когда ваш тип "владеет" неуправляемым ресурсом, либо напрямую (обычно через IntPtr), либо косвенно (например, через Stream, a SqlConnection и т.д.).

Сама коллекция мусора - это только память - с одной маленькой завихренностью. Сборщик мусора может находить объекты, на которые больше нельзя ссылаться, и освободить их. Он не ищет мусора все время, хотя - только тогда, когда он обнаруживает, что ему нужно (например, если одно "поколение" кучи заканчивается из памяти).

Закрутка завершается. Сборщик мусора ведет список объектов, которые уже недоступны, но которые имеют финализатор (написанный как ~Foo() в С#, несколько смутно - они не похожи на деструкторы С++). Он запускает финализаторы на этих объектах, на случай, если им потребуется дополнительная очистка до освобождения их памяти.

Финализаторы почти всегда используются для очистки ресурсов в случае, когда пользователь этого типа забыл распоряжаться им в порядке. Поэтому, если вы откроете FileStream, но забудете позвонить Dispose или Close, финализатор в конечном итоге выпустит для вас основной дескриптор файла. В хорошо написанной программе финализаторы должны почти никогда не срабатывать, на мой взгляд.

Установка переменной в null

Одна маленькая точка при установке переменной в null - это почти никогда не требуется для сбора мусора. Иногда вы можете захотеть сделать это, если это переменная-член, хотя по моему опыту это редкость для "части" объекта больше не требуется. Когда это локальная переменная, JIT обычно достаточно умна (в режиме деблокирования), чтобы знать, когда вы больше не собираетесь использовать ссылку. Например:

StringBuilder sb = new StringBuilder();
sb.Append("Foo");
string x = sb.ToString();

// The string and StringBuilder are already eligible
// for garbage collection here!
int y = 10;
DoSomething(y);

// These aren't helping at all!
x = null;
sb = null;

// Assume that x and sb aren't used here

Одно время, когда стоит установить локальную переменную в null, когда вы находитесь в цикле, и некоторые ветки цикла должны использовать переменную, но вы знаете, что достигли точки, в которой вы этого не делаете. Например:

SomeObject foo = new SomeObject();

for (int i=0; i < 100000; i++)
{
    if (i == 5)
    {
        foo.DoSomething();
        // We're not going to need it again, but the JIT
        // wouldn't spot that
        foo = null;
    }
    else
    {
        // Some other code 
    }
}

Реализация IDisposable/finalizers

Итак, если ваши собственные типы реализуют финализаторы? Почти наверняка нет. Если вы косвенно владеете неуправляемыми ресурсами (например, у вас есть FileStream как переменная-член), то добавление собственного финализатора не поможет: поток почти наверняка будет иметь право на сбор мусора, когда ваш объект, так что вы можете просто полагайтесь на FileStream, имеющий финализатор (при необходимости - он может ссылаться на что-то еще и т.д.). Если вы хотите сохранить неуправляемый ресурс "почти" напрямую, SafeHandle - ваш друг - для этого требуется немного времени, но это означает, что вы почти больше не нужно писать финализатор. Обычно вам нужен только финализатор, если у вас есть действительно прямой дескриптор ресурса (IntPtr), и вы должны перейти к SafeHandle, как только сможете. (Здесь есть две ссылки - читайте оба, в идеале.)

Joe Duffy имеет очень длинный набор рекомендаций вокруг финализаторов и IDisposable (соавторами с большим количеством умных людей), которые стоит прочитать. Стоит осознавать, что если вы запечатываете свои классы, это значительно облегчает жизнь: шаблон переопределения Dispose для вызова нового виртуального метода Dispose(bool) и т.д. Имеет значение только тогда, когда ваш класс предназначен для наследования.

Это было немного странно, но, пожалуйста, попросите уточнить, где вы хотите:)

Ответ 2

Когда вы удаляете объект, ресурсы освобождаются. Когда вы присваиваете значение null переменной, вы просто меняете ссылку.

myclass = null;

После того, как вы выполнили это, объект myclass имел в виду, что все еще существует, и будет продолжаться до тех пор, пока GC не начнет его очищать. Если Dispose явно вызван или он используется в блоке использования, все ресурсы будут освобождены как можно скорее.

Ответ 3

Эти две операции не имеют большого отношения к друг другу. Когда вы устанавливаете ссылку на null, она просто делает это. Это само по себе не влияет на класс, на который вообще ссылались. Ваша переменная просто больше не указывает на объект, к которому она использовалась, но сам объект не изменяется.

Когда вы вызываете Dispose(), это вызов метода для самого объекта. Независимо от метода Dispose, теперь делается на объекте. Но это не влияет на вашу ссылку на объект.

Единственная область перекрытия состоит в том, что, когда больше нет ссылок на объект, в конечном итоге он будет собирать мусор. И если класс реализует интерфейс IDisposable, то Dispose() будет вызываться на объекте до того, как он получит сбор мусора.

Но это не произойдет сразу же после того, как вы установили ссылку на null по двум причинам. Во-первых, могут существовать другие ссылки, поэтому он еще не получит сбор мусора, а во-вторых, даже если это была последняя ссылка, поэтому теперь он готов к сбору мусора, ничего не произойдет, пока сборщик мусора не решит удалить объект.

Вызов Dispose() объекта не "убивает" объект каким-либо образом. Он обычно используется для очистки, чтобы объект можно было безопасно удалить впоследствии, но в конечном итоге нет ничего волшебного в Dispose, это просто метод класса.