В информатике финализатор или метод финализации — это специальный метод , который выполняет финализацию , как правило, некоторую форму очистки. Финализатор выполняется во время уничтожения объекта , до того, как объект будет освобожден , и является дополнительным к инициализатору , который выполняется во время создания объекта , после выделения . Финализаторы настоятельно не рекомендуются некоторыми из-за трудности в правильном использовании и сложности, которую они добавляют, и вместо этого предлагаются альтернативы, в основном шаблон dispose [1] (см. проблемы с финализаторами).
Термин finalizer в основном используется в объектно-ориентированных и функциональных языках программирования , которые используют сборку мусора , архетипом которых является Smalltalk . Это контрастирует с деструктором , который является методом, вызываемым для финализации в языках с детерминированным временем жизни объектов , архетипично C++ . [2] [3] Они, как правило, являются исключительными: язык будет иметь либо финализаторы (если автоматически собирается мусор), либо деструкторы (если память управляется вручную), но в редких случаях язык может иметь и то, и другое, как в C++/CLI и D , а в случае подсчета ссылок (вместо трассировки сборки мусора) терминология различается. В техническом использовании финализатор также может использоваться для обозначения деструкторов, поскольку они также выполняют финализацию, и проводятся некоторые более тонкие различия – см. терминологию. Термин final также указывает на класс, который не может быть унаследован ; это не имеет отношения к делу.
Терминология финализатора и финализации в сравнении с деструктором и уничтожением различается у разных авторов и иногда неясна.
В общем случае деструктор — это метод, вызываемый детерминированно при уничтожении объекта, а его архетипом являются деструкторы C++; в то время как финализатор вызывается недетерминированно сборщиком мусора, а его архетипом являются методы Java . finalize
Для языков, реализующих сборку мусора с помощью подсчета ссылок , терминология различается: некоторые языки, такие как Objective-C и Perl , используют destructor , а другие языки, такие как Python, используют finalizer (согласно спецификации, Python является сборщиком мусора, но эталонная реализация CPython , начиная с версии 2.0, использует комбинацию подсчета ссылок и сборки мусора). Это отражает то, что подсчет ссылок приводит к полудетерминированному времени жизни объекта: для объектов, которые не являются частью цикла, объекты уничтожаются детерминированно, когда счетчик ссылок падает до нуля, но объекты, которые являются частью цикла, уничтожаются недетерминированно, как часть отдельной формы сборки мусора.
В определенном узком техническом использовании конструктор и деструктор являются терминами уровня языка, означающими методы, определенные в классе , в то время как инициализатор и финализатор являются терминами уровня реализации, означающими методы, вызываемые во время создания или уничтожения объекта . Так, например, исходная спецификация языка C# ссылалась на «деструкторы», хотя C# является сборщиком мусора, но спецификация для Common Language Infrastructure (CLI) и реализация ее среды выполнения как Common Language Runtime (CLR) ссылались на «финализаторы». Это отражено в заметках комитета языка C#, в которых, в частности, говорится: «Компилятор C# компилирует деструкторы в ... [вероятно] финализаторы экземпляров[ов]». [4] [5] Эта терминология сбивает с толку, и поэтому более поздние версии спецификации C# ссылаются на метод уровня языка как на «финализаторы». [6]
Другим языком, который не делает такого терминологического различия, является D. Хотя классы D являются сборщиками мусора, их функции очистки называются деструкторами. [7]
Финализация в основном используется для очистки, освобождения памяти или других ресурсов: для освобождения памяти, выделенной с помощью ручного управления памятью ; для очистки ссылок, если используется подсчет ссылок (уменьшение счетчиков ссылок); для освобождения ресурсов, особенно в идиоме получения ресурсов как инициализации (RAII); или для отмены регистрации объекта. Объем финализации значительно различается в зависимости от языка: от обширной финализации в C++, которая имеет ручное управление памятью, подсчет ссылок и детерминированное время жизни объектов; до частого отсутствия финализации в Java, которая имеет недетерминированное время жизни объектов и часто реализуется с помощью трассирующего сборщика мусора. Также возможно, что явной (заданной пользователем) финализации будет мало или вообще не будет, но будет значительная неявная финализация, выполняемая компилятором, интерпретатором или средой выполнения; это распространено в случае автоматического подсчета ссылок, как в ссылочной реализации Python CPython или в автоматическом подсчете ссылок в реализации Objective-C от Apple , которые автоматически разрывают ссылки во время финализации. Финализатор может включать произвольный код; особенно сложным вариантом использования является автоматический возврат объекта в пул объектов .
Освобождение памяти во время финализации распространено в таких языках, как C++, где ручное управление памятью является стандартом, но также происходит в управляемых языках, когда память была выделена за пределами управляемой кучи (вне языка); в Java это происходит с Java Native Interface (JNI) и ByteBuffer
объектами в New I/O (NIO). Последнее может вызвать проблемы из-за того, что сборщик мусора не может отслеживать эти внешние ресурсы, поэтому они не будут собираться достаточно агрессивно, и может вызвать ошибки нехватки памяти из-за исчерпания неуправляемой памяти — этого можно избежать, рассматривая собственную память как ресурс и используя шаблон dispose , как обсуждается ниже.
Финализаторы, как правило, и гораздо менее необходимы, и гораздо реже используются, чем деструкторы. Они гораздо менее необходимы, потому что сборка мусора автоматизирует управление памятью , и гораздо менее используются, потому что они, как правило, не выполняются детерминировано — они могут не быть вызваны своевременно или даже вообще не быть вызваны, а среда выполнения не может быть предсказана — и, таким образом, любая очистка, которая должна быть выполнена детерминированным образом, должна быть выполнена каким-то другим методом, чаще всего вручную с помощью шаблона dispose . Примечательно, что и Java, и Python не гарантируют, что финализаторы когда-либо будут вызваны, и, таким образом, на них нельзя положиться для очистки.
Из-за отсутствия контроля программиста над их выполнением обычно рекомендуется избегать финализаторов для любых операций, кроме самых тривиальных. В частности, операции, часто выполняемые в деструкторах, обычно не подходят для финализаторов. Распространенным антишаблоном является написание финализаторов так, как если бы они были деструкторами, что является как ненужным, так и неэффективным из-за различий между финализаторами и деструкторами. Это особенно распространено среди программистов на C++ , поскольку деструкторы широко используются в идиоматическом C++, следуя идиоме получения ресурсов и инициализации (RAII).
Языки программирования, которые используют финализаторы, включают C++/CLI , C# , Clean , Go , Java , JavaScript и Python . Синтаксис значительно различается в зависимости от языка.
В Java финализатор — это метод finalize
, называемый , который переопределяет Object.finalize
метод. [8]
В JavaScript FinalizationRegistry позволяет запрашивать обратный вызов, когда объект удаляется сборщиком мусора.
В Python финализатор — это метод, называемый __del__
.
В Perl финализатор — это метод, называемый DESTROY
.
В C# финализатор (в более ранних версиях стандарта его называли «деструктором») — это метод, имя которого представляет собой имя класса с ~
префиксом, например ~Foo
, — это тот же синтаксис, что и у деструктора C++ , и эти методы изначально назывались «деструкторами» по аналогии с C++, несмотря на то, что у них было разное поведение, но были переименованы в «финализаторы» из-за возникшей путаницы. [6]
В C++/CLI, где есть как деструкторы, так и финализаторы, деструктор — это метод, имя которого представляет собой имя класса с ~
префиксом, например ~Foo
(как в C#), а финализатор — это метод, имя которого представляет собой имя класса с !
префиксом, например !Foo
.
В Go финализаторы применяются к одному указателю путем вызова runtime.SetFinalizer
функции в стандартной библиотеке. [9]
Финализатор вызывается, когда объект подвергается сборке мусора — после того, как объект становится мусором (недоступным), но до того, как его память будет освобождена. Финализация происходит недетерминированно, по усмотрению сборщика мусора, и может никогда не произойти. Это контрастирует с деструкторами, которые вызываются детерминированно, как только объект больше не используется, и вызываются всегда, за исключением случаев неконтролируемого завершения программы. Финализаторы чаще всего являются методами экземпляра из-за необходимости выполнять операции, специфичные для объекта.
Сборщик мусора также должен учитывать возможность воскрешения объекта. Чаще всего это делается путем предварительного выполнения финализаторов, затем проверки того, были ли воскрешены какие-либо объекты, и если да, то отмены их уничтожения. Эта дополнительная проверка потенциально затратна — простая реализация перепроверяет весь мусор, если хотя бы один объект имеет финализатор — и, таким образом, замедляет и усложняет сборку мусора. По этой причине объекты с финализаторами могут собираться реже, чем объекты без финализаторов (только в определенных циклах), что усугубляет проблемы, вызванные опорой на быструю финализацию, такие как утечки ресурсов.
Если объект воскрешается, возникает еще один вопрос о том, будет ли его финализатор вызван снова, когда он будет уничтожен в следующий раз — в отличие от деструкторов, финализаторы потенциально вызываются несколько раз. Если финализаторы вызываются для воскрешенных объектов, объекты могут многократно воскрешать себя и быть неуничтожимыми; это происходит в реализации CPython Python до Python 3.4 и в языках CLR, таких как C#. Чтобы избежать этого, во многих языках, включая Java, Objective-C (по крайней мере, в последних реализациях Apple) и Python из Python 3.4, объекты финализируются не более одного раза, что требует отслеживания того, был ли объект уже финализирован.
В других случаях, особенно в языках CLR, таких как C#, финализация отслеживается отдельно от самих объектов, и объекты могут многократно регистрироваться или отменяться для финализации.
В зависимости от реализации финализаторы могут вызывать значительное количество проблем, и поэтому их использование настоятельно не рекомендуется рядом органов власти. [10] [11] К таким проблемам относятся: [10]
Кроме того, финализаторы могут не запуститься из-за того, что объекты остаются доступными после того, как они должны быть мусором, либо из-за ошибок программирования, либо из-за неожиданной достижимости. Например, когда Python перехватывает исключение (или исключение не перехватывается в интерактивном режиме), он сохраняет ссылку на стековый фрейм, где было вызвано исключение, что сохраняет активными объекты, на которые ссылаются из этого стекового фрейма.
В Java финализаторы в суперклассе также могут замедлить сборку мусора в подклассе, поскольку финализатор может потенциально ссылаться на поля в подклассе, и, таким образом, поле не может быть удалено сборщиком мусора до следующего цикла после запуска финализатора. [10] Этого можно избежать, используя композицию вместо наследования .
Распространенным антишаблоном является использование финализаторов для освобождения ресурсов по аналогии с идиомой получения ресурсов и инициализации (RAII) в C++: получить ресурс в инициализаторе (конструкторе) и освободить его в финализаторе (деструкторе). Это не работает по ряду причин. В основном финализаторы могут никогда не вызываться, и даже если они вызваны, то не могут быть вызваны своевременно — таким образом, использование финализаторов для освобождения ресурсов, как правило, приводит к утечкам ресурсов . Кроме того, финализаторы не вызываются в предписанном порядке, в то время как ресурсы часто необходимо освобождать в определенном порядке, часто в обратном порядке, в котором они были получены. Кроме того, поскольку финализаторы вызываются по усмотрению сборщика мусора, они часто будут вызываться только при давлении управляемой памяти (когда доступно мало управляемой памяти), независимо от давления ресурсов — если дефицитные ресурсы удерживаются мусором, но доступно много управляемой памяти, сборка мусора может не произойти, таким образом не вернув эти ресурсы.
Таким образом, вместо использования финализаторов для автоматического управления ресурсами, в языках со сборкой мусора вместо этого необходимо вручную управлять ресурсами, как правило, с помощью шаблона dispose . В этом случае ресурсы все еще могут быть получены в инициализаторе, который вызывается явно при создании экземпляра объекта, но освобождаются в методе dispose. Метод dispose может быть вызван явно или неявно языковыми конструкциями, такими как C#'s using
, -with-resources в Java try
или Python's with
.
Однако в некоторых случаях для освобождения ресурсов используются как шаблон dispose, так и финализаторы. Это в основном встречается в языках CLR, таких как C#, где финализация используется в качестве резервной копии для освобождения: когда ресурс получен, приобретающий объект ставится в очередь на финализацию, так что ресурс освобождается при уничтожении объекта, даже если ресурс не освобождается при ручном освобождении.
В языках с детерминированным временем жизни объектов, в частности, в C++, управление ресурсами часто осуществляется путем привязки времени жизни владения ресурсами к времени жизни объекта, получения ресурсов во время инициализации и освобождения их во время финализации; это известно как получение ресурсов при инициализации (RAII). Это гарантирует, что владение ресурсами является инвариантом класса , и что ресурсы освобождаются немедленно при уничтожении объекта.
Однако в языках с недетерминированным временем жизни объектов — которые включают все основные языки со сборкой мусора, такие как C#, Java и Python — это не работает, потому что финализация может быть несвоевременной или может не произойти вообще, и, таким образом, ресурсы могут не быть освобождены в течение длительного времени или даже вообще, что приводит к утечкам ресурсов . В этих языках ресурсы вместо этого обычно управляются вручную с помощью шаблона dispose : ресурсы все еще могут быть получены во время инициализации, но освобождаются путем вызова dispose
метода. Тем не менее, использование финализации для освобождения ресурсов в этих языках является распространенным антишаблоном , и если забыть вызвать, dispose
все равно произойдет утечка ресурсов.
В некоторых случаях обе техники объединяются, используя явный метод dispose, но также освобождая все еще удерживаемые ресурсы во время финализации в качестве резервной копии. Это обычно встречается в C# и реализуется путем регистрации объекта для финализации всякий раз, когда ресурс приобретается, и подавления финализации всякий раз, когда ресурс освобождается.
Если разрешены указанные пользователем финализаторы, финализация может вызвать воскрешение объекта , поскольку финализаторы могут запускать произвольный код, который может создавать ссылки из живых объектов на уничтожаемые объекты. Для языков без сборки мусора это серьезная ошибка, которая приводит к появлению висячих ссылок и нарушениям безопасности памяти ; для языков со сборкой мусора это предотвращается сборщиком мусора, чаще всего путем добавления еще одного шага к сборке мусора (после запуска всех указанных пользователем финализаторов, проверка на воскрешение), что усложняет и замедляет сборку мусора.
Кроме того, воскрешение объекта означает, что объект не может быть уничтожен, и в патологических случаях объект всегда может воскресить себя во время финализации, делая себя неуничтожимым. Чтобы предотвратить это, некоторые языки, такие как Java и Python (начиная с Python 3.4), финализируют объекты только один раз и не финализируют воскрешенные объекты. [ требуется цитата ] Конкретно это делается путем отслеживания того, был ли объект финализирован на основе объект-за-объектом. Objective-C также отслеживает финализацию (по крайней мере, в последних [ когда? ] версиях Apple [ требуется разъяснение ] ) по аналогичным причинам, рассматривая воскрешение как ошибку.
Другой подход используется в .NET Framework , в частности C# и Visual Basic .NET , где финализация отслеживается по "очереди", а не по объекту. В этом случае, если указанный пользователем финализатор предоставлен, по умолчанию объект финализируется только один раз (он ставится в очередь на финализацию при создании и выводится из очереди после финализации), но это можно изменить, вызвав модуль GC
. Финализацию можно предотвратить, вызвав GC.SuppressFinalize
, который выводит объект из очереди, или повторно активировать, вызвав GC.ReRegisterForFinalize
, который ставит объект в очередь. Они особенно используются при использовании финализации для управления ресурсами в качестве дополнения к шаблону удаления или при реализации пула объектов .
Финализация формально дополняет инициализацию — инициализация происходит в начале жизненного цикла, финализация в конце — но на практике существенно отличается. Инициализируются как переменные, так и объекты, в основном для назначения значений, но в целом финализируются только объекты, и в целом нет необходимости очищать значения — память может быть просто освобождена и возвращена операционной системой.
Помимо назначения начальных значений, инициализация в основном используется для получения ресурсов или регистрации объекта в какой-либо службе (например, обработчике событий ). Эти действия имеют симметричные действия освобождения или отмены регистрации, и они могут симметрично обрабатываться в финализаторе, что делается в RAII. Однако во многих языках, особенно в тех, где есть сборка мусора, время жизни объекта асимметрично: создание объекта происходит детерминировано в некоторой явной точке кода, но уничтожение объекта происходит недетерминировано, в некоторой неуказанной среде, по усмотрению сборщика мусора. Эта асимметрия означает, что финализация не может эффективно использоваться как дополнение к инициализации, поскольку она не происходит своевременно, в указанном порядке или в указанной среде. Симметрия частично восстанавливается также путем утилизации объекта в явной точке, но в этом случае утилизация и уничтожение не происходят в одной и той же точке, и объект может находиться в состоянии «утилизирован, но все еще жив», что ослабляет инварианты класса и усложняет использование.
Переменные обычно инициализируются в начале своего жизненного цикла, но не финализируются в конце своего жизненного цикла – хотя если переменная имеет объект в качестве своего значения, объект может быть финализирован. В некоторых случаях переменные также финализируются: расширения GCC позволяют финализировать переменные.
Как отражено в названии, «финализация» и finally
конструкция выполняют схожие цели: выполняют некое финальное действие, обычно очистку, после того, как что-то другое завершилось. Они различаются тем, когда они происходят — finally
предложение выполняется, когда выполнение программы покидает тело связанного try
предложения — это происходит во время раскручивания стека, и, таким образом, есть стек ожидающих finally
предложений, в порядке — в то время как финализация происходит, когда объект уничтожается, что происходит в зависимости от метода управления памятью, и в целом есть просто набор объектов, ожидающих финализации — часто в куче — которая не обязательно должна происходить в каком-либо определенном порядке.
Однако в некоторых случаях они совпадают. В C++ уничтожение объектов детерминировано, и поведение предложения finally
может быть получено с помощью локальной переменной с объектом в качестве значения, область действия которой является блоком, соответствующим телу предложения try
— объект финализируется (уничтожается), когда выполнение выходит из этой области действия, точно так же, как если бы было finally
предложение. По этой причине в C++ нет finally
конструкции — разница в том, что финализация определяется в определении класса как метод деструктора, а не в месте вызова в finally
предложении.
Наоборот, в случае finally
предложения в сопрограмме , как в генераторе Python, сопрограмма может никогда не завершиться — только когда-либо уступая — и, таким образом, при обычном выполнении finally
предложение никогда не выполняется. Если интерпретировать экземпляры сопрограммы как объекты, то предложение finally
можно считать финализатором объекта, и, таким образом, оно может быть выполнено, когда экземпляр подвергается сборке мусора. В терминологии Python определение сопрограммы — это функция-генератор, в то время как ее экземпляр — это итератор-генератор, и, таким образом, finally
предложение в функции-генераторе становится финализатором в итераторах-генераторах, созданных из этой функции.
Понятие финализации как отдельного шага в уничтожении объекта восходит к Монтгомери (1994), [13] по аналогии с более ранним различием инициализации в построении объекта в Мартине и Оделле (1992). [14] Литература до этого момента использовала «уничтожение» для этого процесса, не различая финализацию и освобождение, и языки программирования, относящиеся к этому периоду, такие как C++ и Perl, используют термин «уничтожение». Термины «финализировать» и «финализация» также используются во влиятельной книге Design Patterns (1994). [a] [15] Введение Java в 1995 году содержало finalize
методы, которые популяризировали этот термин и связали его со сборкой мусора, и языки с этого момента обычно проводят это различие и используют термин «финализация», особенно в контексте сборки мусора.
Dispose.
Dispose.
«__del__()
методы должны выполнять абсолютный минимум, необходимый для поддержания внешних инвариантов».