Деструкторы в Ruby

May 20, 2011 headless Ruby деструкторы

Я учил объектно-ориентированное программирование на C++. В C++ одним из ключевых понятий ООП был деструктор – метод, который освободит ресурсы после удаления объекта. Деструкторы были и в PHP.

А в Ruby деструкторов нет (как, наверняка, и во многих других языках).

Причина отсутствия деструкторов в Ruby

Деструкторы в C++ выполняли две функции: освобождение памяти и освобождение внешних ресурсов (файлов, сокетов, подключений к базе и т.п.). Архитектура PHP была всегда заточена под короткие, не задерживающиеся в памяти скрипты, а все ресурсы принудительно освобождались по завершению скрипта – это спасает от ленивых программистов.

Освобождением в памяти в Ruby занимается сборщик мусора. Невозможно принудительно удалить объект из памяти. Внедрение такой возможности заставило бы беспокоиться о битых указателях а-ля C++. Возможно принудительно запустить сбор мусора методом ObjectSpace.garbage_collect, но это никак не гарантирует удаление конкретного объекта.

Поэтому и своевременное освобождение ресурсов невозможно гарантировать.

Вот по причине неопределенности удаления объектов в Ruby нет деструкторов.

Альтернативы деструкторам в Ruby

Приведу пример из моего гема headless. Он запускает виртуальный X-сервер xvfb. Разумеется, этот сервер необходимо рано или поздно остановить, причем вручную. Как решить такую задачу?

Деструкторы по соглашению

“Давайте метод FooClass#destroy будет освобождать ресурсы”.

Этого соглашения в Ruby не существует, например, метод ActiveRecord::Base#destroy не освобождает ресурсы, а удаляет запись из базы (причем ресурсы самого объекта он не особождает вообще).

Тем не менее, при желании можно его ввести для своих классов. Простой пример – метод File.close.

Финализаторы

Если уж очень хочется выполнить действие после удаления объекта, то в Ruby есть финализаторы: ObjectSpace.define_finalizer.

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

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

at_exit

Для выполнения действий по завершению работы программы существует метод Kernel#at_exit. Он добавляет произвольный блок кода в очередь того, что будет выполнено в конце программы.

Как at_exit сочетается с исключениями? Проверим:

at_exit do
  puts 'at_exit'
end

begin
  raise 'Hell'
ensure
  puts 'ensure'
end

Результат работы:

ensure
at_exit
/home/leonid/at_exit.rb:14: Hell (RuntimeError)

Как видишь, во-первых, исключения не мешают работе at_exit, а во-вторых, блок ensure логично выполняется до блока at_exit.

Поэтому у меня Headless обычно подключается так:

headless = Headless.new
at_exit do
  headless.destroy
end

Блочный синтаксис

Еще один удобный способ очищать ресурсы – оборачивать их использование в блок. Например, File.open. Или Headless.ly:

Headless.ly do
  # hot window-on-window action
end

Внутри это выглядит так:

def self.ly
  headless = Headless.start
  yield
  headless.destroy
end

Buy Me a Coffee at ko-fi.com