PHP 8: Weak Maps - Слабые карты

PHP 8: Weak Maps - Слабые карты

теория и практика

PHP 8 версии появились WeakMaps - слабые карты, использующие объекты в качестве ключей

PHP 7.4 представил интересную концепцию слабых ссылок , которые позволяют ссылаться на объект без увеличения его счетчика ссылок. Это немного непонятно и на практике в большинстве случаев не так уж и полезно. На самом деле мы ждали Weak Maps, которые и появились в PHP 8.0.

WeakMap аналогичен SplObjectStorage, и WeakMap и splObjectStorage использует объекты в качестве ключа и позволяет хранить произвольные значения. Тем не менее, WeakMap не защищает объект от "сборки мусора".


$map = new splObjectStorage();

$object = new stdClass();
$map[$object] = 'Foo';

var_dump(count($map)); // int(1)

unset($object);

var_dump(count($map)); // int(1)

В приведенном выше фрагменте splObjectStorage используется для хранения дополнительных данных об объектах. Даже при вызове unset($object) сборщик мусора PHP не удаляет его из памяти из-за ссылки $map splObjectStorage. 

С WeakMap, ссылки являются слабыми, и ссылка внутри WeakMap не мешает сборщику мусора очистить объект, если нет других ссылок на объект, тот же пример:


+ $map = new WeakMap();

$object = new stdClass();
$map[$object] = 'Foo';

var_dump(count($map)); // int(1)

unset($object);

var_dump(count($map)); // int(0)

Как видите WeakMap позволяет очистить объект, а splObjectStorage - нет.

Краткое описание класса WeakMap


final class WeakMap implements ArrayAccess, Countable, IteratorAggregate, Traversable {
public function offsetGet(object $object);
public function offsetSet(object $object, mixed $value): void;
public function offsetExists(object $object): bool;
public function offsetUnset(object $object): void;
public function count(): int;
}

Подробнее о Weak Maps

Слабые карты могут помочь связать временные данные с объектами без изменения состояния самого объекта. Если объект выпадает из области видимости, то же самое происходит и с соответствующими данными.

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

<?php

$a = new Foo();
$b = $a;

//$b and $a теперь отдельные переменные
//оба указывают на объект Foo где-то в памяти.

Каждый раз, когда переменная удаляется, PHP проверяет, есть ли другие переменные, которые все еще ссылаются на этот объект. Если их нет, он знает, что можно удалить этот объект за вас. Этот процесс называется «сборкой мусора», и я здесь сильно упростил процесс, но этого вполне достаточно для общего понимания.

Слабая ссылка же или слабая карта - это способ создания переменной, которая действует как и любая другая, но когда PHP проверяет, указывают ли какие-либо переменные на объект,

эти «слабые» переменные не учитываются. Поэтому, если есть еще три слабые ссылки, указывающие на объект, но нет обычных переменных, PHP с радостью удалит объект и вместо этого установит для остальных переменных значение null.


$map = new WeakMap;
$obj = new stdClass;
$map[$obj] = 42;
var_dump($map);

// object(WeakMap)#1 (1) {
// [0]=>
// array(2) {
// ["key"]=>
// object(stdClass)#2 (0) {
// }
// ["value"]=>
// int(42)
// }
// }

// Здесь объект уничтожается, а ключ автоматически удаляется со слабой карты.
unset($obj);
var_dump($map);
// object(WeakMap)#1 (0) {
// }

  Т.е. по сути Слабые ссылки это способ сказать: 

Я бы предпочел отслеживать это, но если это кажется бесполезным, не храните его только из-за меня

Будет больше смысла увидеть это в более-менее рабочем действии. Предположим, у вас есть серия объектов Product, написанных кем-то там еще в какой-то там библиотеке. Вы не можете их изменить, но вы собираетесь их использовать.

Для каждого из Product вы хотите отслеживать дополнительную информацию, которой нет в исходном объекте, например список объектов Review в этом продукте. Создание подклассов Product для включения обзоров возможно, но беспорядочно, и при наследовании часто можно столкнуться с проблемами при попытке объединить несколько модулей вместе.

Вместо этого мы создадим отдельный объект ReviewList, содержащий слабую карту объектов Review, лениво загружая их по мере необходимости и отслеживая их Product. Как только Product удаляется из памяти, и соотвественно все переменные, которые на него ссылаются, выходят из области видимости, нам не нужно хранить все эти объекты Review. WeakMap в этом случае действует как самоочищающийся кеш и работает аналогично ArrayObject.


<?php
class ReviewList
{
private WeakMap $cache;

public function __construct()
{
$this->cache = new WeakMap();
}

public function getReviews(Product $prod): string
{
return $this->cache[$prod] ??= $this->findReviews($prod->id());
}

protected function findReviews(int $prodId): array
{
// получение обзоров продукта
}
}


$reviewList = new ReviewList();
$prod1 = getProduct(1);
$prod2 = getProduct(2);

$reviewsP1 = $reviewList->getReviews($prod1);
$reviewsP1 = $reviewList->getReviews($prod2);

// ...

$reviewsP1Again = $reviewList->getReviews($prod1);

unset($prod1);

 В этом примере ReviewList есть внутренний «слабый кеш», с ключом объектов Product. Когда вызывается getReviews(), если желаемое значение уже находится в кеше, оно будет возвращено. Если нет, он будет загружен в память, сохранен в кеше WeakMap и затем возвращен. (Здесь есть ??=  - "Оператор присваивания значения NULL", представленный в PHP 7.4, и является исключительно четким именно для такого рода случаев.) Позже, когда мы создадим $reviewsP1Again, значение будет вместо этого искаться в кэше.

Однако в какой-то момент в будущем мы уничтожим $prod1 - unset($prod1). Обычно это не выполняется вручную, но переменная выходит за пределы области видимости и собирает мусор. Поскольку обычных ссылок на объект product 1 больше нет, ссылка на этот объект в $cache Weak Map будет удалена автоматически. Это также приведет Review к автоматической очистке соответствующего списка объектов. Память сохранена, и дальнейшая работа не требуется. Это «Just Works!».

Если вы попытаетесь сделать то же самое с обычным массивом, возникнут две проблемы:

  • Массивы не могут использовать объекты в качестве ключей, поэтому необходимо исключить идентификатор продукта или что-то подобное.
  • Это означает, что кеш не будет знать, что нужно обрезать себя, когда объект, для которого он используется, собирает мусор. Возможно, вам удастся реализовать сложную логику, используя деструкторы, глобальные переменные и прочую черную магию, но… пожалуйста, не делайте этого. Шансы ошибиться высоки, а уровень сложности, который он привносит, того не стоит.

Подобные сценарии кеширования действительно являются единственным надежным вариантом использования WeakMap, но когда вам это нужно, это будет большая экономия памяти и кода.

Замечания по WeakMap

Ключи WeakMap должны быть объектами

Слабые карты допускают только объекты в качестве ключей , а связанные допускают произвольные значения. Попытка сохранить любой другой тип данных в качестве ключа вызовет ошибку типа.


$map = new WeakMap();
$map['Foo'] = 'Bar';

// Fatal error: Uncaught TypeError: WeakMap key must be an object in ...:...

Добавление к WeakMap также не допускается. 


$map = new WeakMap();
$map[] = 'Baz';

// Fatal error: Uncaught Error: Cannot append to WeakMap in ...:...

Error на несуществующих ключах 

Если запрошенный объект не существует в объекте WeakMap, создается исключение \Error.


$map = new WeakMap();
$map[new stdClass()];

// Fatal error: Uncaught Error: Object stdClass#2 not contained in WeakMap in ...:...

Этого можно избежать, проверив индекс на существование.


$map = new WeakMap();
isset($map[new stdClass()]); // false

WeakMap не разрешает свойства 


$map = new WeakMap();
$map->foo = 'Bar';

// Fatal error: Uncaught Error: Cannot create dynamic property WeakMap::$foo in ...:...

WeakMap не поддерживает сериализацию/десериализацию 

Слабые карты не могут быть сериализованы или десериализованы. Если WeakMap класс объявлен final, это тоже можно изменить.


$map = new WeakMap();
serialize($map);

// Fatal error: Uncaught Exception: Serialization of 'WeakMap' is not allowed in ...:...


$serialized_str = 'C:7:"WeakMap":0:{}';
unserialize($serialized_str);

// Fatal error: Uncaught Exception: Unserialization of 'WeakMap' is not allowed in ...:...

Итерация слабых карт  

 Класс WeakMap реализует интерфейс Traversable, его можно использовать итеративно с помощью foreach. Далее WeakMap имплементирует IteratorAggregate, приносящий метод WeakMap::getIterator.


map = new WeakMap();

$obj1 = new stdClass();
$map[$obj1] = 'Object 1';

foreach ($map as $key => $value) {
var_dump($key); // var_dump($obj1)
var_dump($value); // var_dump('Object 1');
}

Сам итератор можно извлечь с помощью метода getIterator, а возвращаемое значение - это iterable.


$map = new WeakMap();

$obj1 = new stdClass();
$map[$obj1] = 'Object 1';

$iterator = $map->getIterator();

foreach ($iterator as $key => $value) {
var_dump($key); // var_dump($obj1)
var_dump($value); // var_dump('Object 1');
}

Влияние на обратную совместимость

Слабые карты - это новая функция в PHP 8.0, и, т.к. в глобальном пространстве имен нет класса с именем WeakMap, то и не должно возникнуть никаких проблем при обновлении существующей базы кода до PHP 8.0.

Поскольку WeakMap это внутренний класс, эту функцию нельзя перенести на предыдущие версии PHP и заставить ее работать с собственным сборщиком мусора. 

Еще раз благодарим Никиту Попова за RFC WeakMap. 

Сергей Мухин

Веб-разработчик со стажем программирования более 6 лет, постоянно учусь, люблю делать новые проекты.

Есть вопросы?

Я почти всегда в режиме онлайн

Связаться со мной