我有一个代码库,其中真正常见的操作
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-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);