Pull to refresh

Comments 56

Интересная история. В Java тоже есть финализаторы и их категорически не рекомендуется использовать. И такого, чтобы разработчики с пятилетним опытом об этом не знали просто не может быть. Возраст платформы наверное сказывается.

Ими просто никто особо не пользуется. Вот и знающих, как оно работает, не густо.

Собственно, не вижу в этом проблемы.
Проблема с финализаторами в том, что одни их неправильно используют, а другие помимо того ещё и пишут статьи типа этой. А надо всего-то в классе с финализатором не держать ничего кроме ссылки на неуправляемый ресурс, а в идеале просто унаследовать SafeHandle.

Эти же люди помимо статей пишут еще и статические анализаторы кода.

Я же правильно понимаю, что Finalize вызовется только когда сработает сборщик мусора, да и то с оговорками?

Когда объект будет помечен на сборку, перейдет в следующее поколение, будет обработан специальным потоком финализации.
В книгах расписано все подробно.

Андерс Хейлсберг. Язык программирования C#, 4-е издание.

Компилятор C# создает из деструктора финализатор. Внутрь финализатора добавляется блок try-finally, и финализатор базового класса вызывается в блоке finally.

Если во время выполнения деструктора происходит исключение, и оно не перехватывается, выполнение деструктора завершается и вызывается деструктор базового класса (если он есть). Если базового класса нет (как в случае с типом object) или в нем отсутствует деструктор, исключение игнорируется.
Исключение игнорируется не всегда, зависит от версии .NET и настройки политики обработки исключений.

Ну то есть пользоваться для освобождения ресурсов этим механизмом нельзя. А, если ресурс ещё занят на момент включения финализатора, то лучшее, что можно сделать это наверное показать пользователю большую красную табличку и ещё отослать разработчикам письмо, что всё сломалось. Но, как я понимаю, надеяться, что этот механизм нормально отработает тоже наивно.

Статья представляет собой смесь дезинформации с рекламой.
Финализаторы нужны только для аварийной очистки системных ресурсов, дабы утечки в вашем проекте не поставили в неудобную позу всю машину — об этом ни слова.
Непосредственно их использовать скорее вредно, чем бесполезно, вот только причина совсем другая:
у наследников CriticalFinalizerObject гарантии выполнения кода завершения лучше, чем у финализатора.
Еще немного матчасти
Особенно печально, что все это уже было на хабре
Очень грустно, если уровень познаний разработчиков PVS Studio о .NET соответствует уровню данной статьи.

Ну и где здесь дезинформация?
Чтобы не было утечек неуправляемых ресурсов, нужно их самому освобождать через Dispose(), а не надеяться на финализаторы.
Возникает вопрос что же нужно использовать: реализовать в своём классе IDisposable или добавить финализатор?

Нет такого вопроса и быть не должно — финализатор и Dispose решают принципиально разные задачи.


Большинство этих способов предполагают реализацию финализатора
… Если разработчик забыл вызвать у объекта метод Dispose(), то в финализаторе можно освободить неуправляемые ресурсы и таким образом избежать их утечки.
Пожалуй, всё.

По вашей логике верхние переливы в раковинах и ваннах тоже не нужны, так как пользователь обязан вовремя закрыть кран. Однако соседи снизу вас не поймут.
Вы скрываете (или сами не знаете) единственную цель финализатора — спасти от последствий утечек не проект (для него в момент вызова уже поздно), а систему в целом.


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

А вот здесь чистейшая деза. Финализаторы не нужны по совсем другой причине — для аварийной очистки есть более надежное место. И, разумеется, классам, не владеющим неуправляемыми ресурсами, финализаторы (и их аналоги) не нужны от слова совсем.

Нет такого вопроса и быть не должно — финализатор и Dispose решают принципиально разные задачи.

Понятно что не должно быть, но такие вопросы задают например на stackoverflow.

Вы скрываете (или сами не знаете) единственную цель финализатора — спасти от последствий утечек не проект (для него в момент вызова уже поздно), а систему в целом.

Спасаясь таким образом от утечек, вы скрываете проблему и её становится сложнее обнаружить

А вот здесь чистейшая деза. Финализаторы не нужны по совсем другой причине — для аварийной очистки есть более надежное место. И, разумеется, классам, не владеющим неуправляемыми ресурсами, финализаторы (и их аналоги) не нужны от слова совсем.

Где здесь деза я не понял. Классам, не владеющим неуправляемыми ресурсами, и IDisposable как правило не нужен, потому что его предназначение «Provides a mechanism for releasing unmanaged resources.»
Классам, не владеющим неуправляемыми ресурсами, и IDisposable как правило не нужен

Это не совсем верно, IDisposable нужен, в том числе, и если вы владеете управляемыми ресурсами реализующими IDisposable. Например Stream.


Финализатор, в свою очередь, нужен в случае, если вы работаете напрямую с IntPtr.

Согласен. Но здесь у вас фактически тоже неуправляемый ресурс, только обёрнутый в IDisposable.
У Stream есть свой финализатор, который о нем позаботится. А вот о неуправляемом ресурсе, от которого все что есть — IntPtr с каким-то адресом система не знает ничего.
Если у вас голый IntPtr, то его нужно освободить в Dispose(), потому что это неуправляемый ресурс.
Если у вас объект типа Stream, то у него тоже нужно позвать Dispose() в своём Dispose(), потому что Stream — это фактически обёртка над неуправляемым ресурсом.
Есть там у Stream свой финализатор или нет, какая разница? В своём Dispose() освобождаем всё что нужно, вот и всё.
Финализатор пишется на случай, когда мы НЕ МОЖЕМ вызвать диспоз. Например когда у нас синглтон, который при закрытии приложения должен корректно завершить работу (записать об этом в базу, например), а пользователь жестко убивает процесс. Финализатор в этом случае как правило отрабатывает, диспоз — нет.

Выше уже писали, что у финализатора и диспоза разный смысл. Диспоз высвобождает ресурсы детерменировано, когда его попросили об этом. Финализатор нужен когда гроб гроб череп крест — приложение падает, и нужно хоть как-то сохранить данные.
Например когда у нас синглтон, который при закрытии приложения должен корректно завершить работу (записать об этом в базу, например), а пользователь жестко убивает процесс. Финализатор в этом случае как правило отрабатывает, диспоз — нет.

Не думаю, что у финализатора больше шансов отработать, чем у dispose'а в случае действительно «жёсткого» завершения работы, например через TerminateProcess. Вот хороший комментарий на эту тему.
Ну, на практике финализатор реально пригождается.

Совершенно реальный пример: есть консольное приложение. Когда оно завершается по ctrl+c всё хорошо, корректно обрабатывается, диспозы, все дела, все отлично.

Как только пользовател закрывает через крестик (а большинство делает именно так), то все становится печально. Есть конечно событие OnConsoleShutdown, и в нем та же диспоз логика есть, но все события в нем не всегда успевают отработать, и начинают вызываться финализаторы. Они тоже ограничены по времени, но есть хотя бы шанс на то, что хоть что-то сохранят. Иногда они не могут (связные объекты были удалены раньше, чем родительские), но в большей части случаев все же отрабатывают.

Ну и да, я согласен, что такой механизм попахивает, но блин, дайте тогда альтернативу. Критика она хороша, но когда единственный воркэраунд это написание kernel-mode кода, в проекте на шарпе, то я лучше уж буду писать финализаторы.
Если у вас голый IntPtr, то его нужно освободить в Dispose(),

Освободить в финализаторе, а в Dispose() освободить и вызвать GC.SuppressFinalize(this), поскольку ресурс уже освобождён и финализация не требуется. Таким нехитрым образом мы обеспечиваем быструю сборку объекта без очереди на финализацию.


При работе со Stream финализатор, разумеется, вообще не нужен.

Грамотным решением при работе с IntPtr было бы унаследоваться от SafeHandle, а не писать велосипед.

Ну, он там примерно так и делает, внутри, вероятно. В общем, да, писать это не нужно, но финализатор нужен именно для этого.

Разница в том, что
— голый IntPtr нужно освобождать и в IDisposable.Dispose(), и в финализаторе.
— Dispose у объекта типа Stream нужно вызывать только в Dispose().

То, что Stream внутри использует неуправляемый ресурс — это ваше предположение (вдруг там MemoryStream) и проблемы самого стрима (его финализатор сам должен закрыть хэндл). Писать код на основе предположений — нельзя :) Обращаться к стриму в финализаторе — нельзя (вдуг его финализатор уже отработал). Делать любых предположений о его состоянии — нельзя.

Проверка на null перед обращением спасет чуть более чем никак.
Так я и не предлагаю писать финализаторы, и тем более обращаться там к стриму.

MemoryStream реализует IDisposable, но не держит неуправляемый ресурс только потому что он является наследником Stream. А если бы он не был его наследником, то и IDisposable у него реализовывать смысла не было бы. Если у вас есть просто Stream и вы не знаете что там, ну или в общем случае объект, реализующий IDisposable, то следует предполагать что там внутри может оказаться неуправляемый ресурс, и поэтому нужно в конце явно позвать Dispose() у этого объекта.
Я не согласен с тем, что закрыть хэндл это проблемы стрима. Это проблемы того кто использует стрим — освободить его через Dispose().
следует предполагать что там внутри может оказаться неуправляемый ресурс, и поэтому нужно в конце явно позвать Dispose() у этого объекта

Вы еще скажите что Subscribe в реактивках возвращает внутри неуправляемый ресурс.
На самом деле не следует делать никаких предположений о том, что именно скрывается за IDisposable


Я не согласен с тем, что закрыть хэндл это проблемы стрима.

Закрыть стрим — проблемы того, кто им пользуется. Освободить неуправляемый ресурс, даже если у самой обертки Dispose не вызван — ее (обертки) прямая обязанность.

Сразу скажу, что Reactive Extensions я никогда не использовал, поэтому могу ошибаться. Я так понял что IDisposable, который возвращается из Subscribe, нужен для того чтобы потом отписаться от нотификаций через вызов Dispose(), а в RxJava аналогичный метод называется unsubscribe(). То есть если мне не нужно отписываться, то я могу и не звать Dispose(). Я не уверен что они правильно выбрали интерфейс IDisposable для этих целей, потому что IDisposable предполагает что ресурс в конце надо задиспозить, а здесь вроде как можно этого и не делать, что сбивает с толку и приводит вот к таким вопросам To Dispose or not to Dispose, that is the question?

Никто не обязан освобождать неуправляемые ресурсы в финализаторе. Если я считаю что финализатор принесёт больше проблем чем пользы, то я могу вообще его не добавлять, а возложить всё это на плечи того кто использует мой объект. В качестве примера вот вам цитата из вашей статьи: «The StreamWriter class owns a Stream object; StreamWriter.Close will flush its buffers and then call Stream.Close. However, if a StreamWriter was not closed, its finalizer cannot flush its buffers. Microsoft „solved“ this problem by not giving StreamWriter a finalizer, hoping that programmers will notice the missing data and deduce their error.»
Никто не обязан освобождать неуправляемые ресурсы в финализаторе.

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


В качестве примера вот вам цитата из вашей статьи

Это ничего, что речь идет не об освобождении ресурсов, а о сохранении изменений?
И кавычки вокруг "solved" тоже наверно просто так добавлены?


То есть если мне не нужно отписываться...

То это означает либо неявное предположение о совпадении времени жизни публикатора и подписчика, либо утечку. Чаще всего второе.

Это должно быть напечатано крупным шрифтом на главной странице вашего статического анализатора.

Причём тут вообще наш анализатор? Он не имеет отношения к этому обсуждению.

Это ничего, что речь идет не об освобождении ресурсов, а о сохранении изменений?

Этот пример про StreamWriter к тому что если финализатор создаст больше потенциальных проблем чем пользы, то от него стоит отказаться. Solved там в кавычках разумеется не просто так, а потому что проблема на самом деле ни разу не solved. Вот что им мешало сбросить недозаписанные байты в файл в финализаторе? Да ничего не мешало, технически это можно сделать, вот только выстрелить себе в ногу при этом очень просто, поэтому и не стали.

Никто не обязан освобождать неуправляемые ресурсы в финализаторе.

Вот вам ещё мнение Эрика Липперта по поводу финализаторов:
Sharp Regrets: Top 10 Worst C# Features
When everything you know is wrong, part one
When everything you know is wrong, part two
This feature is confusing, error-prone, and widely misunderstood.… And in most cases, use of the feature is dangerous, unnecessary, or symptomatic of a bug.
It is therefore very difficult indeed to write a correct finalizer, and the best advice I can give you is to not try.
Причём тут вообще наш анализатор? Он не имеет отношения к этому обсуждению.

Как не имеет? В нем не будет правил, касающихся финализаторов?


Этот пример про StreamWriter к тому что если финализатор создаст больше потенциальных проблем чем пользы, то от него стоит отказаться. Solved там в кавычках разумеется не просто так, а потому что проблема на самом деле ни разу не solved.

То есть отказ от использования финализаторов проблему не решил.


It is therefore very difficult indeed to write a correct finalizer, and the best advice I can give you is to not try.

Есть только один маленький нюанс — чтобы не пробовать писать финализатор на законных основаниях, достаточно использовать SafeHandle и его потомков.


PS: мне ни разу не пришлось писать финализатор. Но сложность его написания аргументом не была никогда.

То есть отказ от использования финализаторов проблему не решил.

А использование финализатора решило бы проблему? Нет, оно потенциально только замаскировало бы её. А вот новые ошибки при неправильно написанном финализаторе вполне можно создать.
Закрыть хэнл, если вдруг он оказался открыт на этапе финализации — это проблемы стрима.

Дело не в том, что вы предлагаете писать или не писать финализаторы. Дело в том, через весь текст статьи проходит мысль «Dispose и Finalize непрерывно связаны, для IDisposable всегда нужно добавлять финализатор».

Т.е. вы вроде и не предлагаете писать финализаторы, но вывод статьи читается вот так:

Прежде чем бросаться добавлять финализаторы для всех классов, реализующих IDisposable, стоит подумать, а действительно ли они так нужны…

Но если вы всё-таки решили использовать финализаторы [для всех классов, реализующих IDisposable, о которых мы тут писали выше], то PVS-Studio… покажет все места в финализаторе, где может возникнуть NullReferenceException [при обращении из финализатора ко вложенным объектам ради вызова у них Dispose].


И вот эти намеки на «есть IDisposable — реализуй финализатор!» продолжаются в комментариях. Вам

Финализатор, в свою очередь, нужен в случае, если вы работаете напрямую с IntPtr.


А вы вроде как и соглашаетесь, но продолжаете намекать на то, что стрим — это неуправляемый ресурс (а неуправляемые ресурсы надо контролировать через финализатор, это все знают!):

Согласен. Но здесь у вас фактически тоже неуправляемый ресурс, только обёрнутый в IDisposable.


Т.е. я то понимаю, как работает Dispose / Finalize. И что есть разница между прямым (IntPtr) и косвенным (IDisposable) владением неуправляемым ресурсом. И вы скорее всего понимаете. И остальные в этом треде понимают. Но это не делает статью лучше, а ворнинг — адекватнее.

Прежде чем бросаться добавлять финализаторы для всех классов, реализующих IDisposable, стоит подумать, а действительно ли они так нужны…

Я так написал, потому что если вы зайдёте на страницу IDisposable в MSDN, например, то там сразу и пример, в котором используется Dispose() вместе с финализатором. И некоторые программисты просто берут и копируют этот пример к себе, а когда спрашиваешь у них зачем тут финализатор, отвечают — потому что паттерн такой.

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

Пример в MSDN относится к классу, владеющему управляемыми и неуправляемыми ресурсами одновременно.
Решается это просто: владение управляемым ресурсом — отдельная ответственность и этим занимаются отдельные классы. Все остальные классы неуправляемыми ресурсами не владеют, следовательно в финализаторах не нуждаются.

Понятно что не должно быть, но такие вопросы задают например на stackoverflow.

И вы даете на него ответ, основанный на заведомо неверной информации.


Спасаясь таким образом от утечек, вы скрываете проблему и её становится сложнее обнаружить

Получение управления финализатором — уже 100% наличие утечки. Сжигать весь дом необязательно — пожарная сигнализация в вашей комнате уже сработала.


Где здесь деза я не понял.

Это печальнее всего. Вы дали заведомо неверную причину для отказа от использования финализаторов. И не поняли этого, имея перед глазами комментарии со ссылками на подробные разъяснения.


Классам, не владеющим неуправляемыми ресурсами, и IDisposable как правило не нужен,

Видите ли, классы, реализующие IDisposable — управляемые ресурсы. Но их владельцы обязаны реализовать IDisposable сами. Такой вариант — 99% случаев. И это, в отличие от финализаторов, часть Junior-минимума
Об управляемых и неуправляемых ресурсах

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

Финализаторы не нужны по совсем другой причине — для аварийной очистки есть более надежное место.


Не очень понятно, для чего по вашему мнению всё-таки подходит использование финализаторов.

Непосредственно их использовать скорее вредно, чем бесполезно


Как по вашему это противоречит содержанию статьи?
Не очень понятно, для чего по вашему мнению всё-таки подходит использование финализаторов.

Ни для чего, поскольку для их целевого назначения есть более подходящее средство.


Как по вашему это противоречит содержанию статьи?

Статья дает рекомендации на базе заведомо неверной информации.

Статья дает рекомендации на базе заведомо неверной информации.

Как я понимаю, вы про это:
Непосредственно их использовать скорее вредно, чем бесполезно, вот только причина совсем другая:
у наследников CriticalFinalizerObject гарантии выполнения кода завершения лучше, чем у финализатора.

Статья должна была сказать, что использовать финализаторы не нужно, потому что есть CriticalFinalizerObject, и нужно всегда использовать его? А разве CriticalFinalizerObject лишён всех минусов, которые перечислены в статье? Может быть статью стоило назвать тогда «Почему не стоит использовать CriticalFinalizerObject»?
Статья должна была сказать...?

В статье не должно быть дезинформации.


Может быть статью стоило назвать тогда «Почему не стоит использовать CriticalFinalizerObject»?

Тогда дезинформация будет прямо в заголовке.
Использовать CriticalFinalizerObject (обычно в виде наследования от одного из его потомков) в классах-обертках для неуправляемых ресурсов не то что "не стоит", а необходимо.

Т.е. если я реализую свой класс, наследуя его от CriticalFinalizerObject и в нём реализую собственный финализатор, то он будет застрахован от всех проблем, описанных здесь? При этом я не говорю сейчас про использование стандартных классов-наследников CriticalFinalizerObject наподобие того-же SafeHandle, тема статьи именно реализация собственных финализаторов.
Т.е. если я реализую свой класс, наследуя его от CriticalFinalizerObject и в нём реализую собственный финализатор, то он будет застрахован от всех проблем, описанных здесь?

А это от вас зависит.
Надо заметить, что прямой наследник от CriticalFinalizerObject находится весьма далеко в рекомендуемом дереве решений:


  1. Не владеем ничем что умеет IDisposable — IDisposable нам и самим не нужен
  2. Владеем только управляемым — нужен IDisposable и ничего более
  3. Хотим владеть неуправляемым — если есть готовая обертка с IDisposable, то смотрим предыдущий пункт, если нет, то следующий.
  4. Делаем управляемую обертку для неуправляемого ресурса — используем максимально специализированный потомок CriticalFinalizerObject.
  5. Нет подходящего потомка — делать нечего, придется наследоваться от CriticalFinalizerObject напрямую и переопределять метод Finalyze

Жесткие требования к завершающему коду не являются основанием для отказа его писать.
Правда у меня за все время работы такой необходимости не возникло ни разу.

Согласен с приведённым вами списком. Вот и автор, на мой взгляд, говорит о том, что для большинства ситуаций подходят пункты 3-4, и не стоит сразу лезть в пункт 5 (а вовсе не противопоставляет IDisposable и финализаторы), как и вы сами:
Правда у меня за все время работы такой необходимости не возникло ни разу.


Если же вы взялись делать пункт 5, то
А это от вас зависит.

перечислены разные способы выстрелить себе в ногу.
Вот и автор, на мой взгляд, говорит о том, что для большинства ситуаций подходят пункты 3-4,

У автора вообще нет ни одного из этих пяти пунктов, я не знаю каким образом вы их вывели из текста статьи.
Зато есть дезинформация про якобы "единственный спорный плюс", а жесткие требования к реализации представлены как "минусы", из-за которых финализаторы писать вроде как не нужно.
Рекомендация в заключении...


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

… просто вредная. Понять, нужен ли финализатор, не просто, а очень просто. Думать же надо о соблюдении требований, когда явный код завершения все-таки нужен.


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


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

У автора вообще нет ни одного из этих пяти пунктов

Так статья и не ставит своей целью осветить все механизмы освобождения ресурсов
жесткие требования к реализации представлены как «минусы», из-за которых финализаторы писать вроде как не нужно.

А разве жёсткие требования не являются «минусами», раз могут приводить к реальным ошибкам? Статья не призывает вообще их не использовать, но что «нужно десять раз подумать» прежде чем это делать.
Понять, нужен ли финализатор, не просто, а очень просто.

Может быть для вас это и так, а на мой взгляд принять решение использовать что-то или нет — это ключевой момент, и перечисленные минусы ещё раз должны напомнить важность этого.
Ну и рекламируемый инструмент статического анализа вместо предупреждения о неверном способе реализации завершающего кода дает подсказки про NRE в финализаторах, которые вообще нет смысла писать.

По вашему потенциально ошибка, приводящая к неожиданному аварийному завершению приложения не является «неверным способом реализации»?
А больше всего огорчает то, что ресурсы компании прямо сейчас тратятся на защиту чести мундира

Если у вас есть конструктивные предложения, как можно улучшить наш продукт и какие ещё паттерны в реализации деструкторов можно детектировать, мы всегда готовы их выслушать.
У автора вообще нет ни одного из этих пяти пунктов

Так статья и не ставит своей целью осветить все механизмы освобождения ресурсов

Так все пять пунктов относятся строго к заявленной теме статьи: "Почему вам не следует использовать финализаторы". Каждый из них дает простой и конкретный ответ, почему в данном варианте использования финализатор не нужен. К "минусам" из статьи ни один из них отношения не имеет.


А разве жёсткие требования не являются «минусами», раз могут приводить к реальным ошибкам?

Нет, не являются.


Статья не призывает вообще их не использовать, но что «нужно десять раз подумать» прежде чем это делать.

Про 10 раз подумать — чистейшая деза. Ответ на сам вопрос "надо ли писать код аварийной очистки?" — простой, на уровне рефлексов. Реализация в пункте 5 сложная, но эта сложность не может служит оправданием отказа от аварийной очистки в обертках для неуправляемых ресурсов.
В результате статья:


  1. Призывает к бесполезным размышлениям над тем, что имеет готовое решение
  2. Поощряет плохой код там, где надо соблюдать требования, а не уклоняться от них.
все это уже было на хабре

А рекламы PVS Studio ещё не было. То есть было, но мало. То есть совсем не мало, а прилично так рекламы этой PVS Studio. Ну, то есть одна сплошная реклама достала уже!

> Если вы в финализаторе освобождаете неуправляемые объекты операционной системы, то ничего плохого не произойдёт в том смысле что при завершении приложения система сама вернёт свои ресурсы.

Система-то ресурсы вернёт, а вот то, что упадёт приложение из-за утечки ресурсов — это и есть плохо.

Освобождение ресурсов в финализаторе позволяет скоменсировать эту утечку, если ресурсы выделяются медленнее, чем отрабатывают финализаторы.
    ~Root()
    {
        StaticRoot = this;
    }
Какой же это страшный кошмар. Это, кажется, не выстрел себе в ногу, а добровольный прыжок в сверхновую.
Расскажите пожалуйста, есть ли хотя бы один пример in the wild самоспасения в финализаторе? Откуда может возникнуть необходимость не дать себя удалить только когда тебя уже собрались удалять?
Вот здесь например пишут о повторном использовании объекта таким способом. Сам я ни разу не встречал на практике
Я правильно понимаю, что описанный паттерн — это костыль, предназначенный для исправления ошибок в коде, а именно отсутствия явного вызова Dispose?

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

public void Dispose()
{
    …
    GC.SuppressFinalize(this);
}

~Holder()
{
    Dispose();
}

Воскрешать объект в финализаторе не нужно. Вместо этого стоит выделить логику тяжёлого ресурса и логику управления этим ресурсов в разные классы: один класс — ресурс, второй класс — холдер, который держит ссылку на ресурс и возвращает ресурс в пул при необходимости.

Возможна ещё алтернативная реализация с использованием WeakReference — тогда вообще можно без финализаторов обойтись.
Я вот грубую ошибку допустил, а вы и не заметили.

Если ссылка на ресурс присутствует только у холдера, то оба эти объекта: и холдер, и ресурс, будут поставлены в очередь на вызов финализатора, что приведёт к негативным последствиям: объект будет переиспользован вызовом Dispose у холдера, но его ресурсы все равно освобождены, т.к. он из очереди на финализацию он не выйдет.

Для того, чтобы этого избежать, фабрика, выделяющая ресурсы, должна хранить жёсткие ссылки на все выделенные объекты. Альтерантивный вариант в виде вызовов SuppressFinalize и ReRegisterForFinalize крайне нежелателен тем, что может легко привести к ошибкам: можно случайно вызвать финализатор 2 раза или не вызвать вообще. Нельзя так обращаться с системыми ресурсами.
Финализатор предназначен для освобождения неуправляемых ресурсов принадлежащих непосредственно текущему объекту. И больше ни для чего другого. Это, кстати, прямо сказано даже в MSDN:
The Finalize method is used to perform cleanup operations on unmanaged resources held by the current object before the object is destroyed

То, что в финализатор полез по ссылке на другой управляемый объект, скорее всего означает что разработчик не понимает разницы межу Dispose и Finalize, и пытается «освобожать» управляемые ресурсы. Или вообще пытается «помочь» сборщику мусора. Вероятность того, что он на самом деле гуру, и использует финализатор не по назначению сознательно, знает обо всех последствиях своего решения… и при этом не проверил на null — ничтожно мала.

Адекватной реакцией коданализа на обращение к reference-свойству было бы «Ты точно знаешь что делаешь? Остановись, почитай про финализаторы! Если твой класс не контролирует неуправляемые ресурсы напрямую — удали финализатор и живи дальше! Если контролирует — используй SafeHandle!»

Еще более адекватным было бы предупреждение на сам факт наличия финализатора. Со страшными минусами из статьи в описании. С отсылкой к тому же SafeHandle. Это действительно предотвратило бы основную ошибку разработчика — реализацию финализатора в случае, когда он совсем не нужен.

А PVS вместо этого говорит разработчику «Все ок, делай финализатор, лазь в нем по другим объектам — это нормально. На null только проверь, и все будет хорошо!»
Возникает вопрос что же нужно использовать: реализовать в своём классе IDisposable или добавить финализатор


Это в каких задачах возникают такой вопрос?
Видимо у меня слишком узкий круг задач. Давно не использовал финализатор.
Во всех задачах, когда нужно освобождать ресурсов, а обертки в которой уже есть финализатор нет.
Sign up to leave a comment.