Это сайт — моя персональная записная книжка. Интересна мне, по большей части, история, своя жизнь и немного программирование.

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 «Гоу», поскольку она очень гармонично решает проблемы процедурного программирования — эффективной работы с процедурным АПИ внешних ресурсов (а в ПХП оно почти всё такое).

9 комментариев
artemp.pip.verisignlabs.com 2014

А try...finally в php нет?

Евгений Степанищев (bolknote.ru) 2014

Комментарий для artemp.pip.verisignlabs.com:

Есть, сделали в PHP 5.5 (то есть недавно совсем), а в PHP 5.6 исправли баг и им можно уже наконец пользоваться (но 5.6 ещё не скоро появится в серверных дистрибутивах — мы, например, на 5.5 только в конце этого года перешли).

Надо, правда, освежить знание что за баг, может с острожностью можно всё-таки будет это использовать.

Евгений Степанищев (bolknote.ru) 2014

Комментарий для Евгения Степанищева:

Закину сам себе ссылку, чтобы почитать и детально разобраться позже: http://habrahabr.ru/post/239435/

Евгений Степанищев (bolknote.ru) 2014

Комментарий для artemp.pip.verisignlabs.com:

в finally можно использовать проверенный код о работе которого всё известно и в котором нет механизма ловли Exception’ов вообще

То есть, в этом случае можно использовать без опаски, спасибо за напоминание. А то я, поверхностно прочитав статью, решил вовсе пока не использовать try…finally в ПХП.

Fyodor Ustinov 2014

Это маленький пример из одной моей реальной программы. Как только управление покинет блок «if»,
вызовется метод Close у profile и файл автоматически закроется.

Это не правда. Метод Close будет вызван когда закончится функция.

Евгений Степанищев (bolknote.ru) 2014

Комментарий для Fyodor Ustinov:

Да, простите, писал рано утром, сейчас исправлю.

Fyodor Ustinov 2014

Комментарий для Евгения Степанищева:

Вообще defer немного особенная штука, про особенности которой надо помнить. Например вот такое поведение иногда оказывается неожиданным:
http://play.golang.org/p/8BSB1vI6z3

А вот такое — иногда удобным:
http://play.golang.org/p/QJ09hu_581

Евгений Степанищев (bolknote.ru) 2014

Комментарий для Fyodor Ustinov:

Для меня первое было ожидаемым, а вот второе — нет. Пока не могу понять как это работает.

Евгений Степанищев (bolknote.ru) 2014

Комментарий для Fyodor Ustinov:

А, ну конечно. Аргумент функции же! Как-то сразу и не сообразил.