Введение

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

Обзор

Хотя основная идея жизненного цикла объекта проста – объект создается, используется, а затем уничтожается – детали существенно различаются между языками программирования и даже в рамках реализации одного и того же языка, и тесно связаны с тем, как реализовано управление памятью. Более того, проводится множество тонких различий между этапами, а также между концепциями, представленными на уровне языка, и концепциями, реализованными на уровне системы. Терминология в целом стандартна, но конкретные этапы, соответствующие тому или иному термину, значительно варьируются в зависимости от языка. Термины обычно образуют антонимичные пары, одна для концепции создания, другая – для соответствующей концепции уничтожения, например, инициализация/финализация или конструктор/деструктор. Пара «создание/уничтожение» также известна как «инициация/завершение» и другими терминами. Также используются термины «выделение» и «освобождение» памяти, по аналогии с управлением памятью, хотя создание и уничтожение объектов может включать в себя гораздо больше, чем просто выделение и освобождение памяти, а выделение/освобождение памяти более корректно рассматривать как этапы создания и уничтожения соответственно.

Детерминизм

Основное различие заключается в том, является ли время жизни объекта детерминированным или недетерминированным. Это зависит от языка, а внутри языка – от способа выделения памяти для объекта; время жизни объекта может отличаться от времени жизни переменной. Объекты со статической аллокацией памяти, в частности объекты, хранящиеся в статических переменных, и модули классов (если классы или модули сами являются объектами и статически аллоцированы), обладают тонким недетерминизмом во многих языках: хотя их время жизни, кажется, совпадает со временем выполнения программы, порядок создания и уничтожения – какой статический объект создан первым, какой вторым и т. д. – обычно недетерминирован. Для объектов с автоматической или динамической аллокацией памяти создание объекта обычно происходит детерминированно, либо явно, когда объект явно создан (например, с помощью `new` в C++ или Java), либо неявно в начале времени жизни переменной, особенно при входе в область видимости автоматической переменной, например, при объявлении. Уничтожение объектов варьируется: в некоторых языках, в частности C++, автоматические и динамические объекты уничтожаются в детерминированные моменты времени, такие как выход из области видимости, явное уничтожение (через ручное управление памятью) или достижение счетчика ссылок нуля; в то время как в других языках, таких как C#, Java и Python, эти объекты уничтожаются в недетерминированные моменты времени, в зависимости от сборщика мусора, и может происходить воскрешение объекта во время уничтожения, продлевающее время жизни. В языках со сборкой мусора объекты обычно динамически аллоцируются (в куче), даже если они изначально связаны с автоматической переменной, в отличие от автоматических переменных с примитивными значениями, которые обычно автоматически аллоцируются (в стеке или в регистре). Это позволяет возвращать объект из функции ("выходить за пределы области видимости") без его уничтожения. Однако в некоторых случаях возможна оптимизация компилятора, а именно анализ выхода за пределы области видимости и доказательство того, что выход невозможен, и, следовательно, объект может быть аллоцирован в стеке; это особенно важно в Java. В этом случае уничтожение объекта произойдет незамедлительно – возможно, даже в течение времени жизни переменной (до окончания ее области видимости), если до него невозможно добраться. Сложный случай – использование пула объектов, где объекты могут быть созданы заранее или повторно использованы, и, следовательно, видимое создание и уничтожение могут не соответствовать фактическому созданию и уничтожению объекта, а только (пере)инициализации при создании и финализации при уничтожении. В этом случае как создание, так и уничтожение могут быть недетерминированными.

Статус во время сотворения и уничтожения

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

Программирование на основе классов

В программировании, основанном на классах, создание объектов также известно как инстанцирование (создание экземпляра класса), а создание и уничтожение могут контролироваться методами, известными как конструктор и деструктор, или инициализатор и финализатор. Создание и уничтожение, таким образом, также известны как конструирование и деструкция, и когда эти методы вызываются, объект, как говорят, конструируется или деструктурируется (не "уничтожается") – соответственно, инициализируется или финализируется при вызове этих методов. Взаимосвязь между этими методами может быть сложной, и язык программирования может иметь как конструкторы, так и инициализаторы (например, Python), или как деструкторы, так и финализаторы (например, C++/CLI), или термины "деструктор" и "финализатор" могут относиться к конструкции на уровне языка по сравнению с реализацией (как в C# по сравнению с CLI). Ключевое различие заключается в том, что конструкторы являются методами класса, поскольку объекта (экземпляра класса) не существует до его создания, а другие методы (деструкторы, инициализаторы и финализаторы) являются методами экземпляра, поскольку объект уже создан. Кроме того, конструкторы и инициализаторы могут принимать аргументы, в то время как деструкторы и финализаторы обычно их не принимают, поскольку они обычно вызываются неявно. В обычном понимании, конструктор – это метод, который непосредственно вызывается пользовательским кодом для создания объекта, в то время как "деструктор" – это подпрограмма, вызываемая (обычно неявно, но иногда и явно) при уничтожении объекта в языках с детерминированным временем жизни объекта – архетипом является C++, а "финализатор" – это подпрограмма, вызываемая неявно сборщиком мусора при уничтожении объекта в языках с недетерминированным временем жизни объекта – архетипом является Java. Этапы финализации существенно различаются в зависимости от управления памятью: при ручном управлении памятью (как в C++ или ручном подсчете ссылок) ссылки должны быть явно уничтожены программистом (ссылки очищены, счетчики ссылок уменьшены); при автоматическом подсчете ссылок это также происходит во время финализации, но автоматизировано (как в Python, после вызова финализаторов, определенных программистом); и при трассирующей сборке мусора это не требуется. Таким образом, при автоматическом подсчете ссылок, финализаторы, определенные программистом, часто короткие или отсутствуют, но может быть выполнена значительная работа, в то время как при трассирующей сборке мусора финализация часто не нужна.

Управление ресурсами

В языках, где время жизни объектов детерминировано, его можно использовать для сопряженного управления ресурсами: это называется идиомой RAII (Resource Acquisition Is Initialization) – ресурсы приобретаются при инициализации и освобождаются при завершении. В языках, где время жизни объектов недетерминировано, особенно из-за сборки мусора, управление памятью обычно отделено от управления другими ресурсами.

Методы создания

Способ создания объектов варьируется в зависимости от языка программирования. В некоторых языках, основанных на классах, специальный метод, известный как конструктор, отвечает за проверку состояния объекта. Как и обычные методы, конструкторы могут быть перегружены, чтобы обеспечить создание объекта с различными заданными атрибутами. Кроме того, конструктор – единственное место, где можно установить состояние неизменяемых объектов. Конструктор копирования – это конструктор, который принимает (единственный) параметр – существующий объект того же типа, что и класс конструктора, и возвращает копию объекта, переданного в качестве параметра. Другие языки программирования, такие как Objective-C, имеют методы класса, которые могут включать методы, подобные конструкторам, но не ограничиваются только инстанцированием объектов. C++ и Java подверглись критике за отсутствие именованных конструкторов – конструктор всегда должен иметь то же имя, что и класс. Это может быть проблематично, если программист хочет предоставить два конструктора с одинаковыми типами аргументов, например, для создания объекта точки либо из декартовых координат, либо из полярных координат, обе из которых могут быть представлены двумя числами с плавающей точкой. Objective-C позволяет обойти эту проблему, поскольку программист может создать класс Point с методами инициализации, например, +newPointWithX:andY: и +newPointWithR:andTheta:. В C++ подобное можно реализовать с помощью статических методов. Конструктор также может относиться к функции, используемой для создания значения тегированного объединения, особенно в функциональных языках программирования.

Уничтожение объекта

Обычно после использования объекта он удаляется из памяти, чтобы освободить место для других программ или объектов. Однако, если памяти достаточно или программа имеет короткое время выполнения, уничтожение объекта может не произойти, а память будет освобождена при завершении процесса. В некоторых случаях уничтожение объекта сводится к освобождению памяти, особенно в языках со сборкой мусора или если "объект" является простой структурой данных. В других случаях перед освобождением памяти выполняется дополнительная работа, например, уничтожение объектов-членов (при ручном управлении памятью) или удаление ссылок на другие объекты для уменьшения счётчиков ссылок (при подсчёте ссылок). Это может происходить автоматически, либо для объекта может быть вызван специальный метод уничтожения. В языках, основанных на классах, с детерминированным временем жизни объектов, в частности в C++, деструктор – это метод, вызываемый при удалении экземпляра класса, перед освобождением памяти. В C++ деструкторы отличаются от конструкторов: их нельзя перегружать, они не должны принимать аргументы, не обязаны поддерживать инварианты класса и могут приводить к завершению программы при выбросе исключений. В языках со сборкой мусора объекты уничтожаются, когда до них больше невозможно добраться из работающего кода. В объектно-ориентированных языках со сборкой мусора аналогом деструкторов являются финализаторы, которые вызываются перед сборкой мусора. Они отличаются непредсказуемым временем и порядком выполнения, поскольку сборка мусора сама по себе непредсказуема, и используются значительно реже и менее сложны, чем деструкторы C++. Примеры таких языков: Java, Python и Ruby. Уничтожение объекта делает все ссылки на него недействительными, а при ручном управлении памятью существующие ссылки становятся висячими. В системах сборки мусора (как при трассировке, так и при подсчёте ссылок) объекты уничтожаются только при отсутствии ссылок на них, но финализация может создавать новые ссылки на объект, и для предотвращения висячих ссылок происходит "воскрешение" объекта, чтобы ссылки оставались валидными.