Defer в PHP
В языке «Гоу» есть превосходная конструкция «defer», которая запускает переданное ей выражение, когда заканчивает выполняться текущий блок. Вот как это может выглядеть:
profile, e := os.OpenFile(oProfile, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0777)
if e == nil {
defer profile.Close()
profile.WriteString("0: 0 0 0 0 ;\n0: 1 8 0 2 ;\n0: 9 63 0 2 ;\n0: 1 63 2 1 ;\n0: 1 63 1 0;")
}
Это маленький пример из одной моей реальной программы. Как только управление покинет текущую функцию, вызовется метод Close у profile и файл автоматически закроется. Пример маленький и всех удобств не раскрывает, но надеюсь они, как на ладони: если у вас много условий при которох управление может покинуть блок, то вам не нужно уничтожать все свои ресурсы в каждой такой ситуации, достаточно их один раз описать через «defer» и всё сработает само.
Одна из причин любить «Гоу».
В «Пайтоне» есть похожая концепция: у класса могут имется «магические» методы «__enter__» и «__exit__», которые срабатывают, соответственно, на входе и выходе в блок, но блок не любой, а специальный:
with open("/etc/passwd", "r") as f:
print(f.read(10))
Объекты, у которых реализованы оба этих метода, могут быть вызываны из специальной конструкции «with», при входе в которую будет вызван метод «__enter__», при выходе — «__exit__», это нагляднее, чем в «Гоу» и тут невозможно забыть освободить какой-то ресурс, но недостаток в том, что надо реализовать эти методы, для какой-то внешней библиотеки нельзя каким-то образом автоматически вызвать тамошний эквивалент «free», придётся всё обернуть в класс.
В ПХП так сделать нельзя, но можно на шаг приблизиться к этому, если использовать деструктор специального объекта, но это и близко не то же самое, что в вышерассмотренных случаях. В «Пайтоне» не зря выделили специальный метод, срабатывающий на выходе, а не обошлись деструкторами, которые (в отличие от «Гоу») в этом языке есть: «__exit__» вызовется гарантированно в момент выхода, тогда как деструктор — когда придётся.
Иногда это может быть очень важным, потому при поздних использованиях того же ресурса, возможно, нам придётся долго ждать когда сборщик мусора доберётся, наконец, до нашего объекта и вызовет деструктор, где освободится повторно нужный нам, но эксклюзивно заблокированный ресурс.
В общем, хорошо, что в ПХП нет такого встроенного класса, его бы сразу начали активно использовать, что привело бы к куче сложноуловимых «багов». Вот это средоточие зла:
class Defer
{
private $funcs;
public function __construct(callable $func)
{
$this->funcs = [$func];
}
public function __destruct()
{
foreach ($this->funcs as $func) {
call_user_func($func);
}
}
public function __invoke(callable $func)
{
array_unshift($this->funcs, $func);
}
}
Очень простой класс с тремя методами. Как же его можно использовать? (никак! не надо! нет!). Пример ниже, он, конечно, мегасинтетический, но что вы хотите от «совы» в семь утра, у меня ещё и мозг-то не работает.
function doSomePHP()
{
for ($i = 0; $i<10; $i++) {
$fp = fopen("/tmp/$i.tmp", "wb");
$zone = new Defer(function() use ($fp) {
fclose($fp);
});
// кстати, обратите внимание, а вот так можно сделать в Hack — это диалект PHP, здорово же:
// $zone = new Defer(() ==> fclose($fp));
// тут какой-то код и куча проверок, причём мы можем выйти в любой момент, а fclose вызовется
fwrite($fp, "My number is $i");
}
}
Что тут происходит? У нас тут, как видно, цикл, в котором открываются на запись 10 файлов, в каждый из них выводится некая строка, а посередине торчит неиспользумая переменная с моим новым объектом. В объект передаётся функция, которая будет вызываться (посмотрите на код объекта) в деструкторе. Как только переменная zone перетрётся (следующим её значением, у нас же цикл или при выходе из функции), объект будет помечен к удалению и когда-нибудь удалится.
Важно заметить две вещи: переменная, содержащая объект, должна затираться в нужное нам время и деструктор вызовется обязательно, но когда — мы не знаем. Первое — недостаток, второе — фатальный недостаток. Оба неустранимые.
Для чего нужен третий метод в класс Defer — «__invoke__»? В языке он появился для поддержки анонимных функций и вызовется, если попытаться вызвать объект, как функцию.
Тут я хочу кивнуть на ещё один хороший язык программирования — «Раст». В нём есть такая интересная штука как «Named lifetimes», не знаю как это перевести литературно, скажем «именованный срок жизни».
Напрягитесь, сейчас будет немного тяжело.
fn select<'r, T>(shape: &'r Shape, threshold: f64,
a: &'r T, b: &'r T) -> &'r T {
if compute_area(shape) > threshold {a} else {b}
}
Выше код на «Расте», взятый из руководства по языку. Тут «’r» — это именованный срок жизни. Сроки жизни всех переменных, помеченных одним именем должны пересекаться. Очень грубо говоря, это значит, что язык не должен уничтожать одну переменную с тем же именем жизни раньше других. Пример выше вызовет ошибку компиляции — видно, что возвращаемое функцией значение пытается пережить остальные переменные с тем же именем — они-то уничтожатся при выходе из функции.
Это позволяет хорошо мирить в языке указатели и сборку мусора, а так же, уже на этапе компиляции ловить некоторые сложные ошибки.
Вернёмся к ПХП. Тут, конечно, всё и близко не так и цели другие, но немного похоже. Упомянутый мною метод был введён для постоения цепочек уничтожения. Например нам надо освободить ресурсы в определённом порядке (например, нужно прибрать за собой временные файлы и только потом убрать блокировку с lock-файла):
function doAnything()
{
$fp = fopen('/var/lock/do.lock', 'r');
flock($fp, LOCK_EX);
$zone = new Defer(function() use ($fp) { fclose($fp); });
// делаем что-то полезное от чего остался мусор
$zone(function() use ($tmpfiles) { array_map('unlink', $tmpfiles); });
// делаем что-то ещё
}
Функции-уничтожители вызовутся в порядке обратном объявлению и каждая последующая не вызовется раньше предыдущей. Вот эта связность и напомнила мне имена жизней в «Расте», хотя аналогия слабоватая, конечно.
В общем, я считаю, что языку ПХП пошла бы на пользу языковая конструкция, близкая к defer «Гоу», поскольку она очень гармонично решает проблемы процедурного программирования — эффективной работы с процедурным АПИ внешних ресурсов (а в ПХП оно почти всё такое).
А try...finally в php нет?
Комментарий для artemp.pip.verisignlabs.com:
Есть, сделали в PHP 5.5 (то есть недавно совсем), а в PHP 5.6 исправли баг и им можно уже наконец пользоваться (но 5.6 ещё не скоро появится в серверных дистрибутивах — мы, например, на 5.5 только в конце этого года перешли).
Надо, правда, освежить знание что за баг, может с острожностью можно всё-таки будет это использовать.
Комментарий для Евгения Степанищева:
Закину сам себе ссылку, чтобы почитать и детально разобраться позже: http://habrahabr.ru/post/239435/
Комментарий для artemp.pip.verisignlabs.com:
То есть, в этом случае можно использовать без опаски, спасибо за напоминание. А то я, поверхностно прочитав статью, решил вовсе пока не использовать try…finally в ПХП.
Это не правда. Метод Close будет вызван когда закончится функция.
Комментарий для Fyodor Ustinov:
Да, простите, писал рано утром, сейчас исправлю.
Комментарий для Евгения Степанищева:
Вообще defer немного особенная штука, про особенности которой надо помнить. Например вот такое поведение иногда оказывается неожиданным:
http://play.golang.org/p/8BSB1vI6z3
А вот такое — иногда удобным:
http://play.golang.org/p/QJ09hu_581
Комментарий для Fyodor Ustinov:
Для меня первое было ожидаемым, а вот второе — нет. Пока не могу понять как это работает.
Комментарий для Fyodor Ustinov:
А, ну конечно. Аргумент функции же! Как-то сразу и не сообразил.