Буквально на днях пришел вопрос от одного из подписчиков касательно одного из постов моего telegram канала. Его смутил вот такой кусок кода
<?php
usort($firstArray, static function($first, $second) {
return $first <=> $second;
});
Вопрос звучал так:
Зачем делать callback’и в функции сортировки (usort), статическими?
И я подумал, что это действительно хороший вопрос, на который стоит обратить внимание.
В чем проблема?
Начнем с определения из документации, чтобы засинхронизироваться:
Анонимные функции, также известные как замыкания (closures), позволяют создавать функции, не имеющие определённых имён. Они наиболее полезны в качестве значений callable-параметров, но также могут иметь и множество других применений.
Анонимные функции реализуются с использованием класса Closure.
Там-же, но это почти никто не читает :
При объявлении в контексте класса, текущий класс будет автоматически связан с ним, делая $this доступным внутри функций класса. Если вы не хотите автоматического связывания с текущим классом, используйте статические анонимные функции.
Выходит, что когда Сlosure объявляется в контексте класса, то класс автоматически привязывается к замыканию. Это означает, что $this
доступен внутри области анонимной функции:
Код, чтобы протестить самостоятельно
<?php
class ExampleTest extends TestCase
{
public function testBasicTest(): void
{
$array = [2, 1];
usort($array, function ($first, $second) {
var_dump($this);
return $first <=> $second;
});
self::assertTrue(true);
}
}
На первый взгляд "да и чёрт с ним", но стоит копнуть чуть глубже.
Замыкание, содержащее ссылку на $this, может быть не обработано сборщиком мусора, что, в свою очередь, может существенно повлиять на производительность.
Вот пример без использования static:
<?php
class LargeObject {
protected $array;
public function __construct() {
$this->array = array_fill(0, 2000, 15);
}
public function getItemProcessor(): Closure {
return function () { // Внутри функции любые вычисления
$a = 1;
$b = 2;
return $a + $b;
};
}
}
function getPeakMemory(): string
{
return sprintf('%.2F MiB', memory_get_peak_usage() / 1024 / 1024);
}
$start = microtime(true);
$processors = [];
for ($i = 0; $i < 2000; $i++) {
$lo = new LargeObject();
$processors[] = $lo->getItemProcessor();
}
var_dump(getPeakMemory());
Как результат, мы получим string(10) "134.10 MiB"
Но в случае, если мы добавим static в 11 строке, то потребление памяти составит string(8) "1.19 MiB"
Всё потому, что в processors[]
мы продолжаем накапливать массив, внутри которого находятся Сlosures которые связаны с классом, а значит, содержат все те данные, которые в нём хранятся.
Выводы
Если подвести короткий итог, то анонимные функции без static стоит использовать если вам необходимо привязать объект к области видимости выполнения функции. Во всех остальных случаях можно и нужно использовать static, как минимум, чтобы случайно не выстрелить себе в ногу.
P.S.
Часто для полноценного поста на Хабре мало короткой заметки. Такие выдержки я публикую в своем телеграм-канале https://t.me/beerphp. Подписывайся и сможешь получить больше интересного материала.
amakhrov
Спасибо, не знал. Действительно, полезно перечитывать документацию.
Но так все же зачем конкретно в колбэке для
usort
(приведенном в начале статьи) static? Просто по привычке (кабы не было чего)?genkovich Автор
Спасибо за фидбек.
Этот коллбек вырван из контекста. Он как раз находился в методе класса
ExampleTest
, который показан на скрине чуть ниже по тексту (там где я сделал дамп и показал, что внутри анонимной функции мы действительно можем обратиться к this.Получается если ты ведешь разработку на фреймворке, то так или иначе ты всё равно будешь находиться внутри класса (будь то контроллер, юзкейс, репозиторий, не важно). Выходит, что в таком случае это уместно использовать везде, где тебе не нужно обращаться к $this.
С другой стороны, если ты просто пишешь скрипт, то static никак не повредит. В целом можно в команде договориться о правиле вроде "Ставь везде static, убирай только там, где обращаешься к $this" (по аналогии с final напр.).
amakhrov
Ну даже в ExampleTest — нет там никакой утечки даже без static, разве не так?
Я бы даже сказал, что утечка будет в каких-то экзотических (синтетических, по большей части) случаях — вроде примера с LargeObject
genkovich Автор
Да, верно, как таковой проблемы в примере нет. А вот насчет синтетических или экзотических случаев я тут не совсем соглашусь. Из моей практики очень часто всплывало то, что "маловероятно" или "такого вообще никогда не произойдет". Особенно эта боль бывает когда обсуждают бизнес требования :) Но ладно, речь о другом. Так вот, введя правило для своей команды из моего предыдущего комментария, как мне кажется, мы избавляем себя от лишней головной боли и можем не думать о том "может ли здесь произойти утечка".
В таком случае она никогда не произойдет, а все места где может — они сделаны осознанно и их легче проконтролировать.
Но здесь каждый разработчик и команда должны сами выбирать, как с этим работать, главное знать, что такая ситуация возможна :)
StepanRodionov
Дополняя ваш комментарий, могу сказать, что можно установить плагин EA Extended в шторме и он будет подсвечивать коллбеки, которые можно безопасно сделать статическими.
vsh797
В php-cs-fixer вроде как можно включить добавление static для всех анонимных функций без использования $this. МБ даже стоит у себя активировать...