编程

WeakMap:PHP 中隐藏的宝石

789 2024-08-01 06:23:00

我们有一个项目,我们将其存储在一个数组中,其中包含类似的项目,如下所示:

/**
 * @template T
 */
class Store
{
    /**
     * @param array<T> $items
     */
    public function __construct(
        protected array $items = []
    ) {
    }

    /**
     * @param T $item
     */
    public function addItem(mixed $item): void
    {
        $this->items[] = $item;
    }
}

目前没有太花哨的东西。Store 应保留配置数量的项目,因此当我们添加新项目并且已达到限制时,我们会删除添加的第一个项目。我们总是对后添加的项目更感兴趣。

这样的事情可以很容易地实现:

/**
 * @template T
 */
class Store
{
    /**
     * @param array<T> $items
     */
    public function __construct(
        protected array $items = [],
        protected int $maxEntries = 200,
    ) {
    }

    /**
     * @param T $item
     */
    public function addItem(mixed $item): void
    {
        $this->items[] = $item;

        if (count($this->items) > $this->maxEntries) {
            array_shift($this->items);
        }
    }
}

现在开始变得复杂了。我们定义了一个名为 Node 的全新结构。Node 可以有子节点,其也就是 Node 。基本上我们是在创建一个树形结构。

事情是这样的:我们的 Node 也有一个项目数组,与我们之前在 Store 中使用的项目相同:

/**
 * @template T
 */
class Node
{
    /**
     * @param array<Node> $children
     * @param array<T> $items
     */
    public function __construct(
        public readonly string $id,
        public array $children = [],
        public array $items = [],
    )
    {
    }
}

我们更新我们的需求,以便在向 store 中添加项目时,我们也会将该项目添加到节点。为了找到节点,我们使用 MagicNodeFinder (我只是为了让这篇文章更清晰而制作的东西)。MagicNodeFinder 将只返回一个应该存储项目的节点,但它取决于它运行的时间,因此它将根据调用的时间返回不同的节点。

我们 Store 现在看起来像这样:

/**
 * @template T
 */
class Store
{
    /**
     * @param array<T> $items
     */
    public function __construct(
        protected array $items = [],
        protected int $maxEntries = 200,
    ) {
    }

    /**
     * @param T $item
     */
    public function addItem(mixed $item): void
    {
        $node = (new MagicNodeFinder())->execute();

        $this->items[] = $item;
        $node->items[] = $item;

        if (count($this->items) > $this->maxEntries) {
            array_shift($this->items);
        }
    }
}

我们需要更新项目删除代码,以便在达到项目限制时可以从节点中删除项目。

虽然这看起来很容易,但事实并非如此,因为不能信任 MagicNodeFinder 返回存储项目的正确节点。

我们无从知道项目存储在哪个节点中,即使我们知道节点,每次需要删除项目时,我们也必须遍历树,从而浪费计算时间

引入 WeakMap

在 PHP 8.0 中,WeakMap 被添入到该语言中。WeakMap 是将对象作为键保存,但不提高该对象的引用计数的映射。因此,每当存储在 WeakMap 中的对象被垃圾回收时(它不再存在,因为它超出了作用域或 unset),它也会立即从映射中删除。

这一切听起来都很困难;让我们来看一个例子。我们重构 Node类如下:

/**
 * @template T
 */
class Node
{
    /**
     * @param string $id
     * @param array<Node> $children
     * @param WeakMap<T, null> $items
     */
    public function __construct(
        public readonly string $id,
        public array $children = [],
        public WeakMap $items = new WeakMap(),
    ) {
    }
}

让我们将一个项目添加到节点中:

$item = new Item('Some info here'); 

$node = new Node(42);

$node->items[$item] = null;

我们计算 WeapMap 中项目的数量:

$node->items->count(); // 1

如果我们通过 unset 垃圾回收项目:

unset($item);

WeakMap 的计数有什么变化?

$node->items->count(); // 0

太酷了!通过垃圾回收该项目,该项目已自动从该 map 中删除。

回到 Store

让我们看看如何在 Store 中使用这个 Weakmap。在 Node 中使用 WeakMap 时,StoreaddItem 方法如下:

/**
 * @param T $item
 */
public function addItem(mixed $item): void
{
    $node = (new MagicNodeFinder())->execute();

    $this->items[] = $item;
    $node->items[$item] = null;

    if (count($this->items) > $this->maxEntries) {
        array_shift($this->items);
    }
}

通过移动 items 数组,由于作用域规则,当到达方法末尾时,对象将被 unset。因此,它也将从 WeakMap 中删除,这很好!完成了;这里不需要额外的更改!

结论

WeakMap 是我以为我永远不会使用 PHP功能,我错了!它使我们的代码性能更高,更易阅读。

 

PHP