Глава 67 · От обычной ссылки к времени жизни кэша
WeakReference и WeakMap: владение, кэш и освобождение объектов
Почему объект остаётся в памяти или исчезает раньше ожидания; слабые ссылки, служебные данные, null и все методы двух классов.
Наблюдать за объектом, не продлевая ему жизнь
Обычный владелец и слабый наблюдатель
$product сохраняет объект, а $weak только позволяет получить его, пока он существует.
- $product = new stdClass()
Обычная переменная удерживает созданный объект.
- WeakReference::create($product)
Создаёт наблюдателя без дополнительного владения целью.
- $weak->get()
Пока цель жива, возвращает тот же объект.
- unset($product)
В простом примере исчезает последний обычный владелец.
- $weak->get() → null
Наблюдатель остался, но цели уже нет.
Это модель владения, а не буквальная схема памяти. Другие переменные, значения WeakMap, замыкания и циклы меняют время жизни объекта.
Долгоживущий обработчик открывает карточки товаров и хранит служебные сведения для отладки. Если служебный список сохраняет каждый объект обычной ссылкой, все карточки остаются в памяти даже после окончания работы с ними. Слабая ссылка позволяет спросить «этот объект ещё существует?» и получить его, пока он нужен кому-то ещё. Она не обязана сохранять объект до вашего следующего обращения.
Сначала вспомним создание объекта. $product = (object) ["title" => "Чай"] преобразует массив в простой объект stdClass с полем title. $editor = $product даёт вторую переменную, через которую доступен тот же объект. unset удаляет конкретную переменную; пока другой владелец сохраняет объект, тот остаётся. WeakReference создаётся специальным методом create, а get возвращает объект или null. Это не PHP-ссылка с символом &: ссылка на переменную решает другую задачу.
$product→ обычная ссылка удерживает объектWeakReference::create($product)→ наблюдатель не удерживает объектunset($product)→ последний владелец исчез$watch->get()→ null, если других владельцев нет
<?php
declare(strict_types=1);
$product = (object) ['title' => 'Чай'];
$watch = WeakReference::create($product);
echo 'Открыта карточка: ', $watch->get()->title, "\n";
unset($product);
echo $watch->get() === null ? "Карточка освобождена; наблюдатель остался\n" : "Карточка ещё нужна\n";
Ожидаемый результат
Открыта карточка: Чай
Карточка освобождена; наблюдатель остался
WeakReference::create создаёт слабого наблюдателя. get возвращает объект, пока есть обычные владельцы, иначе null. unset убирает конкретную переменную, а не уничтожает объект независимо от остальных связей. Для первого результата сильных владельцев больше не было.
<?php
declare(strict_types=1);
$product = (object) ['title' => 'Чай'];
$editor = $product;
$watch = WeakReference::create($product);
unset($product);
echo 'Редактор ещё работает: ', $watch->get()->title, "\n";
unset($editor);
echo $watch->get() === null ? "Последний владелец ушёл: карточка освобождена\n" : "Есть другой владелец\n";
Ожидаемый результат
Редактор ещё работает: Чай
Последний владелец ушёл: карточка освобождена
Обычное присваивание $editor = $product даёт вторую сильную связь. Слабость относится только к watch; наличие WeakReference не отменяет обычных владельцев. Эта модель объясняет, почему unset одной переменной не всегда уменьшает память ожидаемым образом.
Объект можно представить как документ, обычные ссылки — как открытые карточки доступа, а слабую ссылку — как запись «проверь, существует ли документ». Это аналогия владения, не буквальная схема памяти PHP. Удаление переменной не означает принудительное удаление объекта, а сборка циклов может отложить освобождение.
WeakMap: сведения живут столько, сколько объект-ключ
Теперь к каждому объекту товара нужно прикрепить рассчитанную подпись, не меняя класс товара. Обычный PHP-массив принимает ключи int/string, а WeakMap принимает объект. Запись выбирается по идентичности экземпляра, не по одинаковым свойствам. Когда объект-ключ больше никому не нужен, запись удаляется автоматически вместе со служебным значением.
new WeakMap() создаёт пустую карту. $labels[$product] = "Чай — готов к публикации" записывает значение по объекту. Квадратные скобки работают благодаря ArrayAccess, count — благодаря Countable, foreach — благодаря IteratorAggregate. Карта похожа по синтаксису на массив, но её правила ключей и времени жизни другие.
- $product → карточка Чай
- $labels[$product] = «Чай — готов к публикации» → отдельная служебная запись
- unset($product) → больше нет владельцев ключа
- Запись подписи исчезает → count($labels) = 0
<?php
declare(strict_types=1);
$product = (object) ['title' => 'Чай'];
$labels = new WeakMap();
$labels[$product] = 'Чай — готов к публикации';
echo $labels[$product], "\n";
echo 'Подписей в кэше: ', count($labels), "\n";
unset($product);
echo 'После закрытия карточки: ', count($labels), " подписей\n";
Ожидаемый результат
Чай — готов к публикации
Подписей в кэше: 1
После закрытия карточки: 0 подписей
Ключом WeakMap служит объект, а не строка и не его свойства. Запись автоматически исчезает, когда объект-ключ больше не удерживается сильными связями. Здесь count измеряет результат управления жизненным циклом, а не подменяет работу кэша.
<?php
declare(strict_types=1);
$first = (object) ['sku' => 'tea'];
$second = (object) ['sku' => 'tea'];
$labels = new WeakMap();
$labels[$first] = 'Чай: подпись первой карточки';
echo $labels[$first], "\n";
echo isset($labels[$second]) ? "Вторая подпись найдена\n" : "Вторую карточку нужно обработать отдельно\n";
Ожидаемый результат
Чай: подпись первой карточки
Вторую карточку нужно обработать отдельно
Равенство sku не делает экземпляры идентичными. Если кэш нужен по бизнес-ключу tea между разными объектами или запросами, WeakMap не решает эту задачу: используйте обычный индекс/кэш с правилами инвалидирования.
Кэш вычисления: полезен для неизменяемого объекта
Предположим, подпись товара составляется из данных, которые не меняются в течение жизни экземпляра. Функция ниже считает её только при первом обращении и сохраняет в WeakMap. Если свойства объекта могут меняться, автоматическое удаление по времени жизни не исправляет устаревшее значение: потребуется инвалидировать кэш или включить версию данных.
- labelFor($product) → записи пока нет
- Считаем подпись → сохраняем в $labels[$product]
- Второй labelFor с тем же объектом → читаем кэш
- После удаления последнего владельца → карта очищает запись
<?php
class Product {
public function __construct(public readonly string $sku) {}
}
function labelFor(Product $product, WeakMap $labels): string {
if (!isset($labels[$product])) {
echo "Вычисление\n";
$labels[$product] = 'Товар ' . $product->sku;
}
return $labels[$product];
}
$labels = new WeakMap();
$product = new Product('A-10');
echo labelFor($product, $labels), "\n";
echo labelFor($product, $labels), "\n";
unset($product);
echo 'Записей: ', count($labels), "\n";
Ожидаемый результат
Вычисление
Товар A-10
Товар A-10
Записей: 0
public readonly string в параметре конструктора создаёт доступное для чтения свойство; оно получает значение при создании и не переназначается. Функция получает карту объектом: изменение записи видно вызывающему коду без &. Для столь простой конкатенации кэш не нужен; реальная выгода появляется у дорогой повторной обработки, измеренной в вашем проекте.
Когда «слабый» кэш всё-таки удерживает объект
Слабым является именно ключ карты. Значение сохраняется обычным образом. Если это значение содержит объект-ключ, замыкание с use ($object) или другой путь обратно к нему, объект остаётся достижим через саму карту. Аналогично get() из WeakReference отдаёт обычный объект: сохранение результата в переменной временно делает её новым владельцем.
<?php
declare(strict_types=1);
$product = (object) ['title' => 'Чай'];
$watch = WeakReference::create($product);
$cache = new WeakMap();
$cache[$product] = ['product' => $product, 'label' => 'Чай'];
unset($product);
echo $watch->get() === null ? "Карточка освобождена\n" : "Значение кэша всё ещё держит карточку\n";
$held = $watch->get();
unset($cache[$held], $held);
echo $watch->get() === null ? "Обратная связь удалена: карточка освобождена\n" : "Остался владелец\n";
Ожидаемый результат
Значение кэша всё ещё держит карточку
Обратная связь удалена: карточка освобождена
Слабым является ключ WeakMap, но значение хранится обычным образом. Массив в значении содержит сильную связь с тем же товаром. После удаления записи и временной переменной held эта связь исчезает. Обычно храните только производную подпись, которая не ссылается обратно на ключ.
<?php
declare(strict_types=1);
$product = (object) ['title' => 'Чай'];
$watch = WeakReference::create($product);
$temporary = $watch->get();
unset($product);
echo 'Временная обработка: ', $temporary->title, "\n";
unset($temporary);
echo $watch->get() === null ? "Обработка закончена: карточка освобождена\n" : "Карточка ещё нужна\n";
Ожидаемый результат
Временная обработка: Чай
Обработка закончена: карточка освобождена
get не создаёт копию и не возвращает специальный слабый объект. Его результат — обычный доступ к товару; пока temporary хранит его, товар существует. Это полезно на время конкретного действия, но длительное сохранение результата отменит ожидаемую очистку.
После foreach переменная ключа также может удерживать последний объект. Обнулите/удалите её, если она больше не нужна, и проверьте остальные владельцы. Не копируйте все ключи в массив ради диагностики очистки: вы сами создадите сильные ссылки на них.
Сборка циклов, диагностика и выбор контейнера
Фраза «два объекта ссылаются друг на друга, значит они никогда не удалятся» для современного PHP неверна: сборщик умеет находить недостижимые циклы. Но живой контейнер, который по-прежнему хранит объект, не является недостижимым циклом. Сначала выясните, кто владелец, затем выбирайте WeakReference или WeakMap; не заменяйте все ссылки слабыми автоматически.
- Объект хранит ссылку на себя
- Внешняя переменная удалена, остался только цикл
- gc_collect_cycles() собирает недостижимый цикл
- WeakReference::get() возвращает null
<?php
declare(strict_types=1);
gc_enable();
$card = (object) ['title' => 'Чай'];
$card->self = $card;
$watch = WeakReference::create($card);
unset($card);
gc_collect_cycles();
echo $watch->get() === null ? "Недостижимая карточка с циклом освобождена\n" : "Карточка остаётся достижимой\n";
Ожидаемый результат
Недостижимая карточка с циклом освобождена
gc_collect_cycles принудительно запускает поиск недостижимых циклов для наблюдаемого опыта. Это не рекомендация вызывать сборщик после каждой карточки. Живой контейнер с сильной ссылкой — не недостижимый мусор: сначала найдите владельца.
| Потребность | Подходящий инструмент | Кто удерживает объект |
|---|---|---|
| Гарантированно использовать объект позже | Обычное свойство/переменная | Ваш код |
| Спросить, жив ли один объект | WeakReference | Другие владельцы; сама ссылка нет |
| Служебные данные по идентичности объекта | WeakMap | Ключ слабый; значения обычные |
| Коллекция должна владеть участниками | SplObjectStorage | Коллекция сохраняет объекты |
| Найти товар по строковому sku/ID | Массив или явный кэш | Политика вашего контейнера |
WeakReference доступен с PHP 7.4, WeakMap — с PHP 8.0; оба класса встроенные и final. Оба нельзя сериализовать. WeakMap можно клонировать: получится отдельная карта с теми же ключами и обычными правилами копирования значений, а не глубокие копии товаров. С PHP 8.4 диагностический вывод WeakReference показывает наблюдаемый объект или null. Это сведения отладочного вывода, не формат API и не обещание наличия вызываемого метода __debugInfo.
<?php
declare(strict_types=1);
$product = (object) ['title' => 'Чай'];
$draftLabels = new WeakMap();
$draftLabels[$product] = 'Чай: черновик';
$publicLabels = clone $draftLabels;
$publicLabels[$product] = 'Чай: опубликован';
echo $draftLabels[$product], "\n", $publicLabels[$product], "\n";
unset($product);
echo 'После закрытия: ', count($draftLabels), ' черновых и ', count($publicLabels), " публичных подписей\n";
Ожидаемый результат
Чай: черновик
Чай: опубликован
После закрытия: 0 черновых и 0 публичных подписей
Карты разные, поэтому замена строки в одной не изменила другую. Ключом обеих остаётся один product. Когда он исчез, записи исчезли в обеих картах. clone карты не клонирует каждый ключ.
Дальше: клонирование и вложенные объекты, диагностика через Reflection, измерение памяти и времени. Для обычной небольшой страницы заранее добавлять слабые ссылки не требуется: сначала должна появиться конкретная задача владения или кэширования.
Сначала найти владельца, затем выбирать слабую связь
- product → карточка Чай: сильная связь
- watch ⇢ карточка Чай: наблюдение без продления жизни
- get может дать временную сильную связь
- Удаление всех сильных владельцев освобождает карточку
- Наблюдатель после этого возвращает null
Это модель достижимости, а не обещание точного числа байтов памяти после каждой строки. WeakReference полезен наблюдателю, WeakMap — производным сведениям по идентичности объекта; SplObjectStorage нужен, когда контейнер сам должен владеть объектами.
Попробуйте. Оставьте $editor = $product перед unset($product) в примере WeakMap. Когда исчезнет подпись?
Разобрать решение
Она останется, пока editor держит товар. Удалите editor: если других сильных путей нет, карта потеряет запись. Проверьте также значения кэша и замыкания, которые могут содержать обратную связь.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
WeakReference::__constructПрямой new WeakReference запрещён; объект создают через create с конкретной целью.
WeakReference::__construct() — запрещает прямое созданиеПрямой new WeakReference запрещён; объект создают через create с конкретной целью.
| Параметр / результат | Договор |
|---|---|
| Параметры | Нет. |
| Результат | Попытка new WeakReference выбрасывает Error. |
| Правильное создание | WeakReference::create($object) получает существующий объект. |
- new WeakReference → Error
- Создан $product
- create($product) → готовая слабая ссылка
<?php
try { new WeakReference(); }
catch (Error $error) { echo "Используйте create\n"; }
Ожидаемый результат
Используйте create
Сигнатура конструктора присутствует в справочнике, чтобы объяснить запрет, а не предложить рабочий способ создания.
<?php
$product = new stdClass();
$weak = WeakReference::create($product);
echo $weak->get() === $product ? "Наблюдаем этот товар\n" : "Нет объекта\n";
Ожидаемый результат
Наблюдаем этот товар
Статический create вызывается через имя класса и возвращает объект WeakReference. Обычный конструктор создавать вручную не нужно.
Рядом: WeakReference::create, WeakReference::get. Модель владения.
WeakReference::createПолучить слабую ссылку на существующий объект, не продлевая его жизнь.
WeakReference::create(object $object): WeakReferenceПолучить слабую ссылку на существующий объект, не продлевая его жизнь.
| Параметр / результат | Договор |
|---|---|
| $object | Любой объект; массив, строка и число дадут TypeError. |
| Результат | WeakReference. Если слабая ссылка на этот же объект уже существует, возвращается существующий экземпляр WeakReference. |
| Не сохраняет | Владение целью. Созданный на лету объект без других владельцев может исчезнуть сразу. |
- $product существует
- create($product) → $weak
- Повторный create для того же объекта → тот же WeakReference
- Время жизни цели определяют обычные ссылки
<?php
$product = new stdClass();
$first = WeakReference::create($product);
$second = WeakReference::create($product);
var_dump($first === $second);
Ожидаемый результат
bool(true)
Не используйте два вызова create как способ получить два независимых контейнера настроек: для одной цели PHP возвращает тот же наблюдатель.
<?php
declare(strict_types=1);
$lost = WeakReference::create((object) ['title' => 'Чай']);
echo $lost->get() === null ? "Карточка сразу освободилась: владельца нет\n" : "Карточка доступна\n";
$product = (object) ['title' => 'Кофе'];
$owned = WeakReference::create($product);
echo 'Владелец сохранил: ', $owned->get()->title, "\n";
Ожидаемый результат
Карточка сразу освободилась: владельца нет
Владелец сохранил: Кофе
В первом выражении никто не сохраняет обычную связь с новым объектом. WeakReference не становится владельцем, поэтому наблюдать больше нечего. Во втором product держит карточку живой. Это практическая причина создавать владельца отдельно от наблюдателя.
Рядом: WeakReference::get, WeakReference::__construct. Модель владения.
WeakReference::getПолучить живой объект либо null, если цель уже освобождена.
WeakReference::get(): ?objectПолучить живой объект либо null, если цель уже освобождена.
| Параметр / результат | Договор |
|---|---|
| Параметры | Нет. |
| Результат | Тот же объект или null. Обозначение ?object включает оба варианта. |
| Сохранение результата | Полученный объект в обычной переменной удерживается на время её жизни. |
| Практика | Прочитайте один раз в локальную переменную, проверьте null, затем работайте с объектом. |
- get() → $live
- Если null → пропустить необязательное уведомление
- Если объект → прочитать его данные
- unset($live) → больше не удерживаем временно
<?php
$window = (object) ['title' => 'Заказы'];
$weak = WeakReference::create($window);
$live = $weak->get();
if ($live !== null) { echo $live->title, "\n"; }
unset($live, $window);
var_dump($weak->get());
Ожидаемый результат
Заказы
NULL
Так можно обновить ещё существующую модель экрана/подписчика. Если действие обязано случиться даже после закрытия цели, слабая ссылка для него не подходит.
<?php
$subscriber = (object) ['name' => 'Отчёт'];
$weak = WeakReference::create($subscriber);
unset($subscriber);
$live = $weak->get();
echo $live === null ? "Подписчик завершён\n" : "Можно уведомить\n";
Ожидаемый результат
Подписчик завершён
Не обещайте тип object там, где get может вернуть null. Верный договор вашей обёртки — ?object либо явное исключение с понятным смыслом.
Рядом: WeakReference::create, WeakMap::offsetGet. Модель владения.
WeakMap::offsetSetЗаписать или заменить служебное значение по объекту-ключу.
WeakMap::offsetSet(object $object, mixed $value): voidЗаписать или заменить служебное значение по объекту-ключу.
| Параметр / результат | Договор |
|---|---|
| $object | Объект-ключ. Строковые/числовые ключи не поддерживаются. |
| $value | Любое PHP-значение, включая null и объект. Значение удерживается обычной сильной связью. |
| Результат | Ничего не возвращает. Запись $map[$object] = $value делает ту же операцию. |
| Замена / ошибки | Существующая запись заменяется. $map[] = $value без объекта даёт Error; неверный тип аргумента метода — TypeError. |
- $map[$product] = «черновик»
- offsetSet($product, «готово»)
- Количество записей остаётся 1
- offsetGet → «готово»
<?php
$product = new stdClass();
$map = new WeakMap();
$map->offsetSet($product, 'Черновик');
$map[$product] = 'Готово';
echo $map[$product], ': ', count($map), "\n";
Ожидаемый результат
Готово: 1
Строка и метод — два синтаксических способа одной операции. Повторное присваивание не создаёт ещё один экземпляр ключа.
<?php
$product = (object) ['sku' => 'A-10'];
$map = new WeakMap();
$map[$product] = ['label' => 'Товар ' . $product->sku];
echo $map[$product]['label'], "\n";
unset($product);
echo count($map), "\n";
Ожидаемый результат
Товар A-10
0
Значение содержит строку, без обратной ссылки на товар. Если сохранить туда сам $product или замыкание, которое его захватило, карта может удержать ключ через значение.
Рядом: WeakMap::offsetGet, WeakMap::offsetExists, WeakMap::offsetUnset. Модель владения.
WeakMap::offsetGetПрочитать значение по конкретному объекту; отсутствие записи вызывает Error.
WeakMap::offsetGet(object $object): mixedПрочитать значение по конкретному объекту; отсутствие записи вызывает Error.
| Параметр / результат | Договор |
|---|---|
| $object | Объект, для которого ранее создана запись; равный по свойствам другой экземпляр не подходит. |
| Результат | Сохранённое значение любого типа, включая null. |
| Ошибка | Error при отсутствующем ключе; TypeError для не-объекта. |
| Синтаксис | Обычное чтение $map[$object] обращается к той же записи. ?? может задать запасной вариант для отсутствия/null. |
- $product записан с ценой 900
- offsetGet($product) → 900
- Другой new stdClass → записи нет → Error
<?php
$product = new stdClass();
$other = new stdClass();
$map = new WeakMap();
$map[$product] = 900;
echo $map->offsetGet($product), "\n";
try { $map->offsetGet($other); }
catch (Error $error) { echo "Сначала вычислите значение\n"; }
Ожидаемый результат
900
Сначала вычислите значение
Для ожидаемого отсутствия удобнее заранее проверять наличие. Не подавляйте все Error приложения: перехват здесь узкий и демонстрирует конкретный контракт.
<?php
$product = new stdClass();
$map = new WeakMap();
$map[$product] = ['views' => 1];
$map[$product]['views']++;
echo $map[$product]['views'], "\n";
Ожидаемый результат
2
WeakMap поддерживает такое изменение существующей записи через скобки. Это не общее обещание для всех пользовательских ArrayAccess: контракт их offsetGet может отличаться. При проектировании своей коллекции этот нюанс нужно проверять отдельно.
Рядом: WeakMap::offsetSet, WeakMap::offsetExists, WeakMap::getIterator. Модель владения.
WeakMap::offsetExistsПроверить запись с ненулевым значением; null требует отдельной модели наличия.
WeakMap::offsetExists(object $object): boolПроверить запись с ненулевым значением; null требует отдельной модели наличия.
| Параметр / результат | Договор |
|---|---|
| $object | Объект для проверки. Не объект даёт TypeError. |
| Результат на PHP 8.5.11 | true при существующем значении, отличном от null. Отсутствие и сохранённый null дают false; isset($map[$object]) ведёт себя так же. |
| Нюанс документации | Краткое официальное описание говорит о нахождении объекта в карте. Реальное отличие null показано ниже: запись учитывается в count и читается offsetGet, хотя offsetExists false. |
| Для отрицательного кэша | Храните обёртку вроде ["value" => null], если нужно отличать «уже проверено, результата нет» от «ещё не проверяли». |
- offsetSet($product, null)
- count → 1; offsetGet → null
- offsetExists → false
- Обёртка [value => null] → offsetExists true
<?php
declare(strict_types=1);
$product = (object) ['title' => 'Чай'];
$prices = new WeakMap();
$prices[$product] = null;
echo 'Записей проверки цены: ', count($prices), "\n";
echo $prices->offsetExists($product) ? "Есть ненулевой результат\n" : "Ненулевого результата нет\n";
echo $prices->offsetGet($product) === null ? "Проверка сохранила отсутствие цены\n" : "Цена найдена\n";
Ожидаемый результат
Записей проверки цены: 1
Ненулевого результата нет
Проверка сохранила отсутствие цены
offsetExists и isset используют здесь null-чувствительный договор. Запись физически существует и offsetGet возвращает её null. Если нужно отличить «не проверяли» от «проверили, цены нет», храните ненулевую оболочку, показанную следующим примером.
<?php
declare(strict_types=1);
$product = (object) ['title' => 'Чай'];
$prices = new WeakMap();
$prices[$product] = ['value' => null];
if ($prices->offsetExists($product)) {
echo "Повторный запрос поставщику не нужен\n";
echo $prices[$product]['value'] === null ? "Цена пока не опубликована\n" : "Цена получена\n";
}
Ожидаемый результат
Повторный запрос поставщику не нужен
Цена пока не опубликована
Оболочка-массив не равна null, поэтому наличие записи подтверждает выполненную проверку. Внутренний null остаётся отрицательным результатом. В реальном кэше определите срок действия: цена может появиться, пока объект товара продолжает жить.
Рядом: WeakMap::offsetGet, WeakMap::offsetSet, WeakMap::count. Модель владения.
WeakMap::offsetUnsetУдалить одну служебную запись, сохранив сам объект, если у него есть владельцы.
WeakMap::offsetUnset(object $object): voidУдалить одну служебную запись, сохранив сам объект, если у него есть владельцы.
| Параметр / результат | Договор |
|---|---|
| $object | Объект-ключ удаляемой записи. |
| Результат | Возвращаемого значения нет. Отсутствующий объект-ключ не создаёт ошибку. |
| Синтаксис | unset($map[$object]) выполняет ту же операцию. |
| Жизненный цикл | Удаляется связь карты с данными, а не внешний объект. Значение может освободиться, если других владельцев нет. |
- Ключ $product → старая подпись
- offsetUnset($product) → записи больше нет
- $product всё ещё существует
- Следующее обращение пересчитает подпись
<?php
$product = (object) ['title' => 'Старое имя'];
$map = new WeakMap();
$map[$product] = $product->title;
$product->title = 'Новое имя';
$map->offsetUnset($product);
echo $product->title, "\n";
echo count($map), "\n";
Ожидаемый результат
Новое имя
0
Автоматическая очистка по времени жизни не обновляет изменяемый объект. Здесь приложение знает о смене данных и явно удаляет устаревший кэш.
<?php
$product = new stdClass();
$map = new WeakMap();
$map[$product] = 'готово';
unset($map[$product]);
$map->offsetUnset($product);
echo count($map), "\n";
Ожидаемый результат
0
Повторное удаление не требует придумывать временную запись. Но аргумент метода по-прежнему должен быть объектом.
Рядом: WeakMap::offsetSet, WeakMap::count, WeakMap::offsetExists. Модель владения.
WeakMap::countУзнать число текущих живых записей, включая записи со значением null.
WeakMap::count(): intУзнать число текущих живых записей, включая записи со значением null.
| Параметр / результат | Договор |
|---|---|
| Параметры | Нет. |
| Результат | Неотрицательное число записей в карте. count($map) вызывает тот же договор Countable. |
| Изменения | Число уменьшается при удалении записи и при освобождении ключа. |
| Не измеряет | Размер памяти, число всех когда-либо обработанных объектов или количество успешных результатов. |
- Два живых ключа → 2
- Один ключ освобождён → 1
- Оставшаяся запись равна null → всё равно 1
<?php
$first = new stdClass();
$second = new stdClass();
$map = new WeakMap();
$map[$first] = 'готово';
$map[$second] = null;
echo $map->count(), "\n";
unset($first);
echo count($map), "\n";
Ожидаемый результат
2
1
Оставшийся null считается записью. Поэтому count нельзя трактовать как число offsetExists true для всех возможных ключей.
<?php
$product = new stdClass();
$map = new WeakMap();
$map[$product] = 'готово';
foreach ($map as $key => $value) { echo $value, "\n"; }
unset($product);
echo count($map), "\n";
unset($key, $value);
echo count($map), "\n";
Ожидаемый результат
готово
1
0
После цикла $key остался обычной переменной со ссылкой на объект. Карта очистилась только после удаления этого последнего владельца. Так диагностический обход сам может изменить наблюдаемое время жизни.
Рядом: WeakMap::getIterator, WeakMap::offsetUnset, WeakMap::offsetExists. Модель владения.
WeakMap::getIteratorПолучить обход карты; ключами foreach будут объекты, значениями — связанные данные.
WeakMap::getIterator(): IteratorПолучить обход карты; ключами foreach будут объекты, значениями — связанные данные.
| Параметр / результат | Договор |
|---|---|
| Параметры | Нет. |
| Результат | Внешний объект итератора для карты. Официальная страница указывает Exception при неудаче получения. |
| Обычное использование | foreach ($map as $object => $value) запрашивает обход автоматически. |
| Осторожно | Прочитанный ключ становится обычной ссылкой в переменной. Обход не является неизменяемым снимком для бизнес-отчёта. |
- Карта содержит $product → «готово»
- getIterator() даёт обход
- foreach получает $object и $value
- После цикла освобождаем временные ссылки
<?php
$product = (object) ['sku' => 'A-10'];
$map = new WeakMap();
$map[$product] = 'Готово';
$iterator = $map->getIterator();
foreach ($iterator as $object => $label) { echo $object->sku, ': ', $label, "\n"; }
unset($iterator, $object, $label);
Ожидаемый результат
A-10: Готово
Здесь $object — сам товар, не его числовой ID. Не передавайте такой итератор в обычное преобразование массива с сохранением ключей: массив PHP не принимает объект-ключ.
<?php
$product = (object) ['sku' => 'A-10'];
$map = new WeakMap();
$map[$product] = 'Готово';
$rows = [];
foreach ($map as $object => $label) { $rows[] = ['sku' => $object->sku, 'label' => $label]; }
unset($object, $label);
echo json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR), "\n";
Ожидаемый результат
[{"sku":"A-10","label":"Готово"}]
Для отчёта выбраны стабильные скалярные данные. Полученный список уже имеет собственное время жизни и расход памяти; экспорт всех записей снова создаёт полную коллекцию.
Рядом: WeakMap::count, WeakMap::offsetGet, WeakMap::offsetUnset. Модель владения.
Источники и обсуждения
- PHP: WeakReference
- PHP: WeakMap
- PHP: сборка циклов
- PHP 8.5.11: реализация слабых ссылок и карты
- Обсуждение: Временная цель слабой ссылки исчезает; владение нужно выразить явно. — обсуждение проблемы
- Обсуждение: Для ключа-объекта нужен объектный контейнер, не PHP-массив. — обсуждение проблемы
- Обсуждение: Сильная коллекция удерживает объекты, пока они остаются её участниками. — обсуждение проблемы
- Обсуждение: Очистка коллекции и очистка внешних владельцев — разные шаги. — обсуждение проблемы
- Обсуждение: Диагностика владения не должна превращаться в логику на точном счётчике ссылок. — обсуждение проблемы
- Обсуждение: Освобождение объекта и уменьшение памяти процесса не одно и то же. — обсуждение проблемы