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


108

Я очарован тем, как работают CLR и GC (я работаю над расширением своих знаний, читая CLR через C #, книги / сообщения Джона Скита и многое другое).

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

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

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

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

using (MyDisposableObj mydispobj = new MyDisposableObj())
{

}

Классы потоков (например, BinaryWriter) имеют метод Finalize? Зачем мне это использовать?

Ответы:


210

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

Dispose, сборка мусора и финализация

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

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

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

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

Финализаторы почти всегда используются для очистки ресурсов в том случае, если пользователь типа забыл избавиться от них упорядоченным образом. Поэтому, если вы открываете, 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это ваш друг - это займет немного времени, но это означает, что вам почти никогда не придется снова писать финализатор . Обычно финализатор нужен только в том случае, если у вас есть действительно прямой дескриптор ресурса (an IntPtr), и вам следует перейти кSafeHandleтак скоро как сможешь. (Там две ссылки - в идеале прочтите обе.)

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

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


Re: «Один раз, когда может быть стоит установить для локальной переменной значение null» - возможно, также некоторые из более сложных сценариев «захвата» (несколько захватов одной и той же переменной) - но, возможно, не стоит усложнять пост! +1 ...
Марк Гравелл

@Marc: Это правда - я даже не думал о захваченных переменных. Хм. Да, думаю, я оставлю это в покое;)
Джон Скит

не могли бы вы сказать, что произойдет, если вы установите "foo = null" в приведенном выше фрагменте кода? Насколько мне известно, эта строка очищает только значение переменной, указывающей на объект foo в управляемой куче? так что вопрос в том, что там будет с объектом foo? Разве мы не должны избавиться от этого?
odiseh

@odiseh: Если бы объект был одноразовым, то да - вы должны его утилизировать. Тем не менее, этот раздел ответа касался только сборки мусора, которая является совершенно отдельной.
Джон Скит,

1
Я искал разъяснения по поводу некоторых проблем с IDisposable, поэтому я поискал в Google "IDisposable Skeet" и нашел это. Большой! : D
Maciej Wozniak

22

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

myclass = null;

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


7
Он может не существовать после выполнения этой строки - возможно, до этой строки он был собран сборщиком мусора . JIT умен - делать подобные строки почти всегда неуместными.
Джон Скит,

6
Установка значения null может означать, что ресурсы, удерживаемые объектом, никогда не освобождаются. Сборщик мусора не удаляет, он только завершает работу, поэтому, если объект напрямую содержит неуправляемые ресурсы, а его финализатор не удаляет (или у него нет финализатора), то эти ресурсы будут протекать. Что-то, о чем нужно знать.
LukeH

6

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

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

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

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

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


Я думаю, что этот ответ дополняет или представляет собой деталь ответа «рекурсивный».
dance2die
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.