PHP生成器产生第一个值,然后遍历其余值

Lay*_*rás 8 php yield generator

我有以下代码:

<?php

function generator() {
    yield 'First value';
    for ($i = 1; $i <= 3; $i++) {
        yield $i;
    }
}

$gen = generator();

$first = $gen->current();

echo $first . '<br/>';

//$gen->next();

foreach ($gen as $value) {
    echo $value . '<br/>';
}
Run Code Online (Sandbox Code Playgroud)

输出:

First value
First value
1
2
3
Run Code Online (Sandbox Code Playgroud)

我需要“第一个价值”才能产生一次。如果我取消注释$ gen-> next()行,则会发生致命错误:

致命错误:消息为“无法倒带已运行的生成器”的未捕获异常“ Exception”

我该如何解决?

don*_*ote 10

chumkiu 的回答是正确的。一些额外的想法。

提案 0:剩余()装饰器。

(这是我在这里添加的最新版本,但可能是最好的)

PHP 7+:

function remaining(\Generator $generator) {
    yield from $generator;
}
Run Code Online (Sandbox Code Playgroud)

PHP 5.5+ < 7:

function remaining(\Generator $generator) {
    for (; $generator->valid(); $generator->next()) {
        yield $generator->current();
    }
}
Run Code Online (Sandbox Code Playgroud)

用法(所有 PHP 版本):

function foo() {
  for ($i = 0; $i < 5; ++$i) {
    yield $i;
  }
}

$gen = foo();
if (!$gen->valid()) {
  // Not even the first item exists.
  return;
}
$first = $gen->current();
$gen->next();

$values = [];
foreach (remaining($gen) as $value) {
  $values[] = $value;
}
Run Code Online (Sandbox Code Playgroud)

可能会有一些间接开销。但从语义上讲,我认为这是非常优雅的。

建议 1:for() 而不是 while()。

作为一个不错的语法替代方案,我建议使用for()而不是while()减少->next()调用和初始化的混乱。

简单版本,没有您的初始值:

for ($gen = generator(); $gen->valid(); $gen->next()) {
  echo $gen->current();
}
Run Code Online (Sandbox Code Playgroud)

使用初始值:

$gen = generator();

if (!$gen->valid()) {
    echo "Not even the first value exists.<br/>";
    return;
}

$first = $gen->current();

echo $first . '<br/>';
$gen->next();

for (; $gen->valid(); $gen->next()) {
    echo $gen->current() . '<br/>';
}
Run Code Online (Sandbox Code Playgroud)

您可以将第一个$gen->next()放入for()语句中,但我认为这不会增加太多可读性。


我在本地(使用 PHP 5.6)做的一个小基准测试表明,这个带有 for() 或 while() 的版本显式调用 ->next()、current() 等比带有foreach(generator() as $value).

建议 2:generator() 函数中的 Offset 参数

这仅在您可以控制生成器功能时才有效。

function generator($offset = 0) {
    if ($offset <= 0) {
        yield 'First value';
        $offset = 1;
    }
    for ($i = $offset; $i <= 3; $i++) {
        yield $i;
    }
}

foreach (generator() as $firstValue) {
  print "First: " . $firstValue . "\n";
  break;
}

foreach (generator(1) as value) {
  print $value . "\n";
}
Run Code Online (Sandbox Code Playgroud)

这意味着任何初始化都会运行两次。也许并不理想。

它还允许像 generator(9999) 这样的调用具有非常高的跳过次数。例如,有人可以使用它来分块处理生成器序列。但是每次从 0 开始然后跳过大量项目在性能方面似乎真的是一个坏主意。例如,如果数据来自文件,则跳过意味着读取 + 忽略文件的前 9999 行。


Luc*_*one 8

问题是foreach 尝试重置(倒带)发生器。但是rewind()如果生成器当前位于第一个收益率之后,则会引发异常。

因此,您应避免foreach使用并while改为使用

$gen = generator();

$first = $gen->current();

echo $first . '<br/>';
$gen->next();

while ($gen->valid()) {
    echo $gen->current() . '<br/>';
    $gen->next();
}
Run Code Online (Sandbox Code Playgroud)