Также существует проблема циклических ссылок, когда есть, например, пара объектов, причём первый имеет ссылку на второй, а второй имеет ссылку на первый. В этом случае, даже если ни на один из объектов пары ни у кого в целом по программе не осталось ссылок, они будут жить до конца времён.
В простейшем случае проблема циклических ссылок возникает и с одним объектом.
Попробуйте такой объект:
- Код: Выделить всё
Private Self As Object
Private Sub Class_Initialize()
MsgBox "Я родился"
Set Self = Me
End Sub
Private Sub Class_Terminate()
MsgBox "Я умер"
End Sub
Вы не увидите сообение «Я умер», потому что событие Class_Terminate никогда не произойдёт, ибо экземпляр держит ссылку на самого себя, не давая счётчику достигнуть нулевого значения.
Эта ситуация в общем случае считается вредной.
Может ли этот эффект быть полезным?
Зададимся пока другим вопросом: может ли объект отменить собственное уничтожение?
Например у формы есть события Unload и QueryUnload, имеющие аргумент Cancel. Обработчик события может присвоить аргументу ненулевое значение, и это отменит выгрузку формы.
Может ли аналогичным образом экземпляр класса отменить собственное уничтожение из обработчика Class_Terminate? На первый взгляд кажется, что не может, ведь у события Terminate нет никакого Cancel-аргумента (аналогичного тому, что есть у событий Unload и QueryUnload).
Но на самом деле — может.
Когда собственный счётчик ссылок объекта достигает значения 0, VB вызывает обработчик Class_Terminate, чтобы экземпляр класса мог сделать какие-то последние действия перед своей смертью. Но дело в том, что после того, как происходит возврат из Class_Terminate, VB ещё раз проверяет счётчик ссылок.
И если после этого счётчик ссылок имеет ненулевое значение, уничтожение объекта отменяется.
Это напрямую не документированное, но единственно-допустимое поведение: ведь код внутри Class_Terminate мог ссылку на объект передать или присвоить куда-то (куда угодно), что в результате могло увеличить счётчик ссылок на объект. И если после того, как произошёл возврат из Class_Terminate и у кого-то вновь появилась ссылка на объект, объект будет уничтожен, это означало бы приговор для приложения, ибо оно вскоре рухнет.
Вспомним то, что объект может держать ссылку сам на себя и предотвращать таким образом собственную смерть.
— Так можно ли из Class_Terminate отменить предстоящее скорое уничтожение объекта?
— Да, достаточно присвоить ссылку на самого себя приватной переменной объекта.
Примерно так:
- Код: Выделить всё
Option Explicit
Private SelfBootstrap As Class1
...
Private Sub Class_Terminate()
Set SelfBootstrap = Me
End Sub
Экземпляр такого класса не уничтожится после того, как последняя внешняя ссылка на него будет затёрта с помощью Set ref = Nothing.
И в чём польза?
Казалось бы, общий эффект от такого кода ничем не отличается от самого первого примера, где объект присваивает ссылку на самого себя своей собственной внутренней переменной, и становится вечно-живущим. И никакой пользы от вечно-живущих объектов нет, ибо они только утечку памяти создают. Где это вообще может быть применено с пользой?
Во-первых, разница есть, а во-вторых, есть целое семейство ситуаций, когда этот трюк способен обеспечить правильную логику работы приложения.
Представьте себе, что ваш класс управляет некими ресурсами, которые при смерти экземпляра должны освобождаться и уничтожаться. Обычно ресурсы выделяются и активируются в Class_Initialize, либо при первом обращении к какому-то публичному члену класса (подход с отложенной инициализацией). А уничтожаются и освобождаются внутри Class_Terminate.
Но есть целый класс ситуаций, когда освобождение ресурсов выполняется не единственным вызовом одной функции, а когда оно выполняется в несколько стадий и асинхронно. И если вы в Class_Terminate поместите вызов, который инициирует первую стадию освобождения ресурсов, то сразу же после возврата из Class_Terminate ваш объект уничтожится, и некому будет дождаться завершения первой стадии и начать вторую стадию освобождения ресурсов.
А это приведёт к тому, что ресурсы не будут освобождены как полагается в полном объёме.
Было бы решением сделать у класса некий метод CloseAll, который будет инициировать и дожидаться завершения всех стадий освобождения связанных с экземпляром сторонних ресурсов, и потребовать у пользователя класса, чтобы тот неприменно вызывал CloseAll, прежде чем занулит свою ссылку на объект. Но это совершенно не решение и оно не стыкуется с сутью COM: когда обладатель ссылки на объект зануляет свою ссылку на объект, он не имеет никаких средств узнать, является ли он последним обладателем ссылки на объект (и значит должен перед занулением ссылки вызвать CloseAll), либо кто-то ещё в данный момент держит ссылку на объект и может полноценно пользоваться им (и тогда вызов CloseAll будет преждевременным и всё сломает).
К тому же, нельзя не понимать, что программист — существо забывчивое, и скорее всего забудет вызвать CloseAll, либо не сможет предусмотреть все варианты того, как пойдёт выполнение кода, и останется вариант, в котором CloseAll не вызывается перед смертью объекта.
Поэтому подход с явным и принудительным освобождением ресурсов имеет право на жизнь, но в любом случае класс обязан уметь подчищать за собой перед самоуничтожением всё, что не было освобождено принудительным ручным вызовом.
Здесь и приходит на помощь наш трюк.
Внутри Class_Terminate объект проверяет, был ли перед этим вызван метод, выполняющий принудительное освобождение ресурсов, и освобождены ли все ресурсы. Если всё освобождено — объект не препятствует собственной смерти. Но если что-то по-прежнему не освобождено, объект внутри Class_Terminate присваивает ссылку на самого себя своей приватной переменной, и таким образом блокирует своё скорейшее освобождение.
Далее он имеет возможность дождаться момента, когда завершатся все стадии освобождения занятых ресурсов, и в конце-концов сбросить ссылку SelfBootstrap — и умереть окончательно.
Что это за ресурсы, которые освобождаются асинхронно и в несколько стадий, так, что окончательного освобождения ресурсов нужно дожидаться? Приведу два примера.
Допустим, ваш класс сам порорждает окна (с помощью CreateWindowEx). Допустим, с каждым окном связан некий блок памяти, хранящий какие-то данные, относящиеся к этому окну. Теперь представьте, что наступает момент, когда затирается последняя ссылка на экземпляр такого класса. В этот момент созданное ранее окно должно уничтожиться, а память, выделенная под «некий блок памяти, хранящие ассоциированные с окном данные» — освободиться.
Но уничтожение окна — это не та операция, которая выполняется мгновенно и за раз.
Если вы вызвали DestroyWindow(), то это ещё не значит, что созданное окно тут же перестало существовать. После того, как вы вызвали DestroyWindow, окно продолжает существовать, но в очередь сообщений потока приходят специфические сообщения (WM_DESTROY, а затем WM_NCDESTROY). До тех пор, пока эти сообщения не будут обработаны потоком, окно продолжает существовать, HWND остаётся валидным. Теоретически возможно, что на момент вызова DestroyWindow() в очереди сообщений уже и так есть какие-то рядовые ещё не обработанные сообщения, предназначенные окну. Либо, что такие сообщения поступят в очередь после вызова DestroyWindow(), но до обработки WM_DESTROY. Все эти рядовые сообщения должны быть обработаны, прежде чем дело дойдёт до WM_DESTROY и WM_NCDESTROY. И до тех пор, пока дело не дойдёт до WM_NCDESTROY, наш экземпляр не имеет права умирать, равно как и не имеет права освобождать блок данных, ассоциированных с окном (этим блоком не обязательно должен быть блок, выделенный с помощью HeapAlloc/VirtualAlloc — речь может идти о простом массиве внутри экземпляра или просто о наборе приватных членов класса).
И это, в общем, как рза наш случай: вызвать в Class_Teminate функцию DestroyWindow и сразу же умереть — это глупейшее, что можно сделать. Правильным будет вызвать её и подтянув себя за шнурки, то есть установив SelfBootstrap = Me, отсрочить свою гибель до момента, когда будет обработано WM_NCDESTROY.
Пример с окном оказался неактуальным: я сам себя ввёл в заблуждение неудачным экспериментом. На самом деле, WM_DESTROY и WM_NCDESTROY доставляются с помощью SendMessage, а не PostMessage, и поэтому будут обработаны до выхода из DestroyWindow.- Другой пример, не связанный с окнами и оконными сообщениями — это работа с мультимедиа. Как осуществляется вывод или захват звука в Windows (либо вывод или приём MIDI-сообщений)? Обычно так: вы подготавливаете некий буфер или набор буферов и отдаёте их системе. Если вы выводите данные, то вы сперва заполняете буфер данными, прежде чем отдать его системе. Если принимаете — то отдаёте буфер системе, и ждёте, пока система не даст вам отмашку, что она заполнила буфер данными.
С момента, когда вы отдали буфер системе, вы не имеете права его трогать, в частности — освобождать выделенную под буфер память. Вне зависимости от того, принимаете вы данные или отправляете, буфер попадает обратно в ваше распоряжение только тогда, когда система явно вам его возвращает (об этом она оповестит вас одним из нескольких поддерживаемых способов — по вашему выбору).
Что, если у вас класс, который занимается отправкой или приёмом мультимедийных данных (звука или MIDI-сообщений), и внезапно зануляется последняя ссылка на экземпляр такого класса? Класс не может уничтожиться, не освободив буферы, ибо это приведёт к утечке памяти. В то же время, класс не может освободить буферы сейчас же, если они были переданы в распоряжение системы и не были возвращены ею.
При выводе данных система возвращает нам буферы по мере того, как она воспроизвела записанные в буферы данные. При захвате поступающих снаружи данных — по мере того, как предоставленные ей нами буферы были заполнены поступающими данными.
Так или иначе, если наш объект собрался умереть, он не может умереть прямо сейчас, не дождавшись сперва, пока система не вернёт все буферы в наше распоряжение. При происхождении Class_Terminate, мы должны просигнализировать системе, что мы хотим свернуть нашу деятельность по захвату/воспроизведению мультимедийных данных. Система постарается прекратить эту деятельность и вернуть нам буферы. Как быстро она это сделает — зависит от реализации мультимедийных драйверов. Лишь когда мы получим от системы отмашку, что буферы возвращены нам (например сообщения MM_WOM_DONE, MM_WIM_DATA, MM_MOM_DONE, MM_MIM_LONGDATA), наш экземпляр будет иметь право освободить буферы (если они не были частью данных самого экземпляра) и окончательно умереть.
Разумеется, случаи, когда ресурсы освобождаются не мгновенно и не синхронно (то есть не блокируя поток, инициирующий освобождение ресурса до момента его освобождения), не ограничиваются окнами и API библиотеки WinMM.
Напоследок скажу, что нужно иметь, что Class_Terminate вызывается только единожды — когда в первый раз счётчик ссылок достигает нулевого значения. Если объект предпочтёт продлить своё существование поддержкой себя за шнурки, когда он отпустит шнурок — второй раз Class_Terminate уже не сработает.