如何在嵌套的yield语句内迭代SplObjectContainer,同时保留外部迭代?

问题描述 投票:0回答:1

我有一个代码库,其中真正常见的操作

findAll
是在SplObjectStorage支持的实现上实现的。有时需要以嵌套方式多次
findAll
(循环遍历集合中的每个条目) - 但如果不手动跟踪每个堆栈帧中的状态以重置外部迭代,似乎没有简单的方法可以做到这一点回到其原始状态 - 或保留单独的迭代计数。

这与使用其迭代器实现时相同,但请注意,如果我刚刚使用了一个数组,您实际上可以以堆栈安全的方式循环它们,不会干扰正在进行的迭代 - 手动移动其内部时情况并非如此迭代指针。

<?php

$objects = new \SplObjectStorage();
$objects->attach((object)[0]);
$objects->attach((object)[1]);
$objects->attach((object)[2]);
$objects->attach((object)[3]);
$objects->attach((object)[4]);

$array = [ 0, 1, 2, 3, 4 ];

function iterate($objects) {
    $objects->rewind();
    
    while ($objects->valid()) {
        $object = $objects->current();
        
        yield ((array)$object)[0];
        
        $objects->next();
    }
}

function iterate2($objects) {
    foreach ($objects as $object) {
        yield ((array)$object)[0];
    }
}

function iterate3(array &$array) {
    foreach ($array as $v) {
        yield $v;
    }
}

function iterate4(array &$array) {
    reset($array);
    
    while (current($array) !== false) {
        
        yield current($array);
        
        next($array);
    }
}

echo "iterate:\n";

foreach (iterate($objects) as $a) {
    foreach (iterate($objects) as $b) {
        echo "$a -> $b\n";
    }
}

echo "iterate2:\n";


foreach (iterate2($objects) as $a) {
    foreach (iterate2($objects) as $b) {
        echo "$a -> $b\n";
    }
}

echo "iterate3:\n";

foreach (iterate3($array) as $a) {
    foreach (iterate3($array) as $b) {
        echo "$a -> $b\n";
    }
}

echo "iterate4:\n";


foreach (iterate4($array) as $a) {
    foreach (iterate4($array) as $b) {
        echo "$a -> $b\n";
    }
}

输出:

iterate:
0 -> 0
0 -> 1
0 -> 2
0 -> 3
0 -> 4
iterate2:
0 -> 0
0 -> 1
0 -> 2
0 -> 3
0 -> 4
iterate3:
0 -> 0
0 -> 1
0 -> 2
0 -> 3
0 -> 4
1 -> 0
1 -> 1
1 -> 2
1 -> 3
1 -> 4
2 -> 0
2 -> 1
2 -> 2
2 -> 3
2 -> 4
3 -> 0
3 -> 1
3 -> 2
3 -> 3
3 -> 4
4 -> 0
4 -> 1
4 -> 2
4 -> 3
4 -> 4
iterate4:
0 -> 0
0 -> 1
0 -> 2
0 -> 3
0 -> 4

基本上,最后一个内部迭代将迭代移动到停止外部迭代的最后一个元素,因此如果不手动跟踪最后一个迭代并重置为该值,则无法恢复。

显然,一种解决方法是首先消耗整个 SplObjectStorage,然后使用

->offsetGet
手动迭代它来手动跟踪迭代,但这种方法在几个方面感觉缺乏。

以下是我尝试解决该问题的一些方法:

克隆存储

function iterate($objects) {
    $objects = clone $objects;
    $objects->rewind();
    
    while ($objects->valid()) {
        $object = $objects->current();
        
        yield ((array)$object)[0];
        
        $objects->next();
    }
}

出于多种原因,我对这种方法不满意,但它非常干净,我更喜欢性能更高的方法。

保存前一个密钥并使用 next() 进行更正

function iterate($objects) {
    $objects->rewind();
    
    while ($objects->valid()) {
        $object = $objects->current();
        
        $before = $objects->key();
        yield ((array)$object)[0];
        
        
        if ($objects->key() > $before) {
            $objects->rewind();
        }
        
        if ($objects->key() < $before) {
            while ($objects->key() !== $before)
                $objects->next();
        }
        
        $objects->next();
    }
}

这在某些方面更好,但在其他方面更糟糕,当yield修改原始容器时,它们都不足,并且在存储上使用foreach时,它们都不适合,而且我真的看不出是否有一个将其拼凑在一起的逻辑方法。

解决这个问题的“正确”或干净的方法是什么?

php generator
1个回答
0
投票

为了将来的参考,我最终将我的方法与一些重要的修复和调整相结合。

应该注意的是,许多库(例如 php-ds polyfills)使用普通数组作为存储支持,在嵌套的 Yield 上下文中访问时,它们的行为正常,并且一些迭代器实现(例如 SplQueue/SplDoubleLinkedList)实际上完全具有接下来计算的不同基本方法 - 请参阅

ext/spl/spl_dllist.c#L834

但是在类实例上保留内部迭代指针并正常递增它的简单方法(就像此处看到的 SplObjectStorage 一样)

ext/spl/spl_observer.c#L749
似乎都遇到了这个问题。

我们依赖于有序键(例如整数索引)的“重新寻求收益”方法可以概括为:

readonly class KeySeekableIterator implements \SeekableIterator {
    public function __construct(private \Iterator $iterator) {}
    
    public function key(): int { return $this->iterator->key(); }
    public function current(): mixed { return $this->iterator->current(); }
    public function next(): void { $this->iterator->next(); }
    public function rewind(): void { $this->iterator->rewind(); }
    public function valid(): bool { return $this->iterator->valid(); }
    
    public function seek(int $offset): void {
        if ($offset < 0)
            throw new \InvalidArgumentException("Offset needs to be >= 0");
        
        if ($this->key() > $offset) {
            $this->rewind();
        }
        
        while ($this->key() < $offset) {
            $this->next();
            
            if (!$this->valid())
                throw new \OutOfBoundsException();
        }
    }
    
}

readonly class ContinuousIterator implements \IteratorAggregate {
    private \SeekableIterator $iterator;
    
    public static function asSeekableIterator(\Iterator $iterator): \SeekableIterator {
        if ($iterator instanceof \SeekableIterator)
            return $iterator;
        
        return new KeySeekableIterator($iterator);
    }
    
    public function __construct(\Iterator $iterator) {
        $this->iterator = self::asSeekableIterator($iterator);
    }
    
    public function getIterator(): \Traversable {
        foreach ($this->iterator as $key => $value) {   
            try {
                yield $key => $value;
            } finally {
                $this->iterator->seek($key);
            }
        }
    }
}

@Sammitch 也支持简单地克隆迭代器的方法,每次迭代都可以这样封装:

readonly class ImmutableIterator implements \IteratorAggregate {
    public function __construct(private \Iterator $iterator) {
    }
    
    public function getIterator(): \Traversable {
        yield from clone $this->iterator;
    }
}

在我自己的代码库中,我最终用一些标志扩展了函数来选择使用什么方法,但这些包装类的优点是它们与标准迭代器库的其余部分很好地契合,并且可以与 FilterIterator 之类的东西无缝组合等等

这里有一些您可以使用的额外测试

function val($obj) {
    if ($obj instanceof \stdClass) {
        return ((array)$obj)[0];
    }
    
    return $obj;
}

function test(iterable $iterator) {
    foreach ($iterator as $a) {
        foreach ($iterator as $b) {
            echo val($a) . " -> " . val($b) . PHP_EOL;
        }
    }
}

/*
// SplObjectStorage
$objects = new \SplObjectStorage();
$objects->attach((object)[0]);
$objects->attach((object)[1]);
$objects->attach((object)[2]);
$objects->attach((object)[3]);
$objects->attach((object)[4]);
*/

/*
// SplQueue / SplDoubleLinkedList
$objects = new \SplQueue();
$objects[] = 0;
$objects[] = 1;
$objects[] = 2;
$objects[] = 3;
$objects[] = 4;
*/

// ArrayIterator
$objects = new \ArrayIterator([0,1,2,3,4]);

echo "Plain:\n";
test($objects);


echo "Immutable:\n";
$iter = new ImmutableIterator($objects);
test($iter);

echo "Continuous:\n";
$iter = new ContinuousIterator($objects);
test($iter);
© www.soinside.com 2019 - 2024. All rights reserved.