Необычный паттерн: объект-самоубийца
В D, как известно, нет встроенного способа удалить объект – то есть, освободить занятую им память. Функция destroy лишь вызывает деструктор и помечает объект как недействительный, но фактически память высвобождается в следующем цикле сборки мусора. dlib, будучи библиотекой для разработки приложений реального времени, предоставляет альтернативные механизмы управления памятью с возможностью удалять объекты вручную – в моменты, явно определяемые программистом, а не логикой сборщика мусора. Это накладывает на программиста определенную степень ответственности, так как стопроцентно ручное управление памятью – занятие довольно хардкорное. Я написал на Medium статью на эту тему, где описал парадигму владения (ownership), рекомендуемую при работе с dlib. Суть ее в том, что удаление данных автоматически выполняет объект-владелец этих данных, когда кто-то – вы сами или его собственный владелец – удаляет его самого. Таким образом, вы у себя в коде расставляете единичные функции Delete только в ключевых местах, когда ваше приложение переходит из одного режима в другой, а вся рутинная работа по удалению данных ложится на иерархию объектов-владельцев. Например, если это игра, то вы можете удалить текущую сцену, когда пользователь завершает уровнень, проигрывает, выходит в главное меню или загружает сохранение. Если объект сцены является владельцем всех ее данных, то они будут автоматически удалены.
Но при этом может возникнуть неожиданная проблема. Допустим, у вас есть некий глобальный менеджер игры, который каждый раз передает управление загруженной сцене. Сцена формирует нужные ей структуры данных, обрабатывает входящие события, реагирует на пользовательский ввод, обновляет изменяемое состояние и рендерит графику – то есть, совершает довольно много задач в цикле, полагаясь на то, что все ее данные находятся в памяти. Если вам нужно завершить работу сцены, то это равносильно удалению объектом самого себя – то есть, сцена обращается к корневому менеджеру с запросом о переключении в другой режим, и он ее удаляет. Можно ли в dlib так делать?
На первый взгляд, можно:
import dlib.core.memory;
import dlib.core.ownership;
class Host: Owner
{
this()
{
super(null);
}
void kill(Owned obj)
{
deleteOwnedObject(obj);
}
}
class Property: Owned
{
Host host;
this(Host host)
{
this.host = host;
host.addOwnedObject(this);
}
void kill()
{
host.kill(this);
}
}
void main()
{
Host host = New!Host;
Property prop = New!Property(host);
prop.kill();
Delete(host);
}
Но это работает только в простейшем случае, если вы не обращаетесь к данным экземпляра Property после удаления. Это звучит странно – кому придет в голову что-то делать с удаленным объектом? Но не все так просто. Рассмотрим такую модификацию нашего примера:
class Property: Owned
{
Host host;
string[5] data;
this(Host host)
{
host.addOwnedObject(this);
this.host = host;
data = ["one", "two", "three", "four", "five"];
}
void doSomethingComplex()
{
foreach(i, v; data)
{
if (i == 3)
host.kill(this);
else
writeln(v);
}
}
}
void main()
{
Host host = New!Host;
Property prop = New!Property(host);
prop.doSomethingComplex();
Delete(host);
}
Цикл в методе doSomethingComplex продолжится даже после удаления объекта – получается “зомби-метод”, поведение которого непредсказуемо. У меня, например, массив data просто исчез, и writeln печатает пустую строку.
Очевидно, что корректно было бы прервать цикл сразу после удаления. Но что, если удаление происходит где-то в недрах Host’а, и информация об этом к вам в цикл не возвращается? У меня в движке, например, многие классы слушают внешние события, и удаление сцены может произойти в виртуальном методе-обработчике события. При этом абстрактный класс слушателя событий, естественно, не учитывает возможность подобного самоубийства и не проверяет собственное существование. Сделать подобную проверку вообще не так-то просто, и выполнять ее перед любым телодвижением довольно накладно. Получается, что паттерн “самоубийцы” в его самой очевидной реализации плохо совместим с принципами ООП.
Что же делать? Выручает асинхронность! Вместо того, чтобы удалять объект сразу, его лучше поместить в некий “карантин” и удалить позже, когда он завершит свою работу. Например, хост может удалить его на следующей итерации игрового цикла. В нашем примере это будет выглядеть так:
import dlib.container.array;
class Host: Owner
{
Array!Owned objectsToKill;
this()
{
super(null);
}
void killAsync(Owned obj)
{
objectsToKill.append(obj);
}
void killObjects()
{
foreach(obj; objectsToKill)
deleteOwnedObject(obj);
objectsToKill.free();
}
}
class Property: Owned
{
Host host;
string[5] data;
this(Host host)
{
host.addOwnedObject(this);
this.host = host;
data = ["one", "two", "three", "four", "five"];
}
void doSomethingComplex()
{
foreach(i, v; data)
{
if (i == 3)
host.killAsync(this);
else
writeln(v);
}
}
}
void main()
{
Host host = New!Host;
Property prop = New!Property(host);
prop.doSomethingComplex();
host.killObjects();
Delete(host);
}
Получается простейшее подобие сборщика мусора, поведение которого вам полностью подконтрольно.