PHP - Generator,send不遵循yield命令

时间:2014-04-15 22:27:57

标签: php generator yield

我想逐步编写示例代码,如何分离生成器中的任务并将它们移动到2个或更多生成器中,以实现它们之间的协作式多任务处理。您可以找到关于此here的所有测试。

生成器在某种程度上是合乎逻辑的,但是我被一步所困,我无法解释为什么它以这种方式工作:

发电机:

    $spy = new Object();
    $spy->tasks = array();

    $createGenerator = function ($i1) use ($spy) {

        yield; //(* -> task 1)
        $spy->tasks[] = $i1;
        yield($i1); //(task 1 -> *)

        $i1 = yield; //(* -> task 2)
        //task 2
        $i2 = $i1 + 1;
        $spy->tasks[] = $i2;
        yield($i2); //(task 2 -> *)

        $i2 = yield; //(* -> task 3)
        $i3 = $i2 + 1;
        $spy->tasks[] = $i3;
        yield($i3); //(task 3 -> *)

        $i3 = yield; //(* -> task 4)
        $i4 = $i3 + 1;
        $spy->tasks[] = $i4;
        yield($i4); //(task 4 -> *)

        $i4 = yield; //(* -> task 5)
        $i5 = $i4 + 1;
        $spy->tasks[] = $i5;
        yield($i5); //(task 5 -> *)

    };

我等待成功的测试,但失败了:

    /** @var Generator $generator */
    $generator = $createGenerator(1);

    $i1 = $generator->send(null);
    $generator->send($i1);
    $i2 = $generator->send(null);
    $generator->send($i2);
    $i3 = $generator->send(null);
    $generator->send($i3);
    $i4 = $generator->send(null);
    $generator->send($i4);
    $i5 = $generator->send(null);

    $this->assertSame($spy->tasks, array(1, 2, 3, 4, 5));
    $this->assertSame(array($i1, $i2, $i3, $i4, $i5), array(1, 2, 3, 4, 5));

意外成功的测试:

    /** @var Generator $generator */
    $generator = $createGenerator(1);

    $i1 = $generator->send(null);
    $generator->send(null); //blank sends needed to skip the yield-yield gaps
    $i2 = $generator->send($i1);
    $generator->send(null);
    $i3 = $generator->send($i2);
    $generator->send(null);
    $i4 = $generator->send($i3);
    $generator->send(null);
    $i5 = $generator->send($i4);

    $this->assertSame($spy->tasks, array(1, 2, 3, 4, 5));
    $this->assertSame(array($i1, $i2, $i3, $i4, $i5), array(1, 2, 3, 4, 5));

你能用双yield向我解释生成器的奇怪行为吗?

结论:

send()始终将代码从yield的输入运行到下一个yield的输出。因此,通过使用Generator运行send(),它始终以输入开头,这就是为什么您无法使用yield获取第一个send()的输出,这就是为什么在null进入无效状态之前,你总是在send()之前获得Generator返回值的原因...遗憾的是PHP手册缺少这个信息......

2 个答案:

答案 0 :(得分:1)

工作示例

测试生成器的工作示例:

$spy = new stdClass();
$spy->tasks = array();

$createGenerator = function ($i1) use ($spy) {
    yield;
    $spy->tasks[] = $i1;
    $i1 = (yield $i1);

    yield;
    $i2 = $i1 + 1;
    $spy->tasks[] = $i2;
    $i2 = (yield $i2);

    yield;
    $i3 = $i2 + 1;
    $spy->tasks[] = $i3;
    $i3 = (yield $i3);

    yield;
    $i4 = $i3 + 1;
    $spy->tasks[] = $i4;
    $i4 = (yield $i4);

    yield;
    $i5 = $i4 + 1;
    $spy->tasks[] = $i5;
    (yield $i5);
};

你的考试:

$generator = $createGenerator(1);

$i1 = $generator->send(null);
$generator->send($i1);
$i2 = $generator->send(null);
$generator->send($i2);
$i3 = $generator->send(null);
$generator->send($i3);
$i4 = $generator->send(null);
$generator->send($i4);
$i5 = $generator->send(null);

print_r($spy);
for ($i = 1; $i <= 5; ++$i) {
    echo ${'i'.$i} . "\n";
}

这给出了期望的结果:

stdClass Object
(
    [tasks] => Array
        (
            [0] => 1
            [1] => 2
            [2] => 3
            [3] => 4
            [4] => 5
        )

)
1
2
3
4
5

进一步提示

有关详细信息,请参阅the manual on the send method,其中总结了send的工作原理:

  

将当前值作为当前结果发送给生成器   yield表达式并恢复生成器的执行。

     

如果此方法为,则生成器不在yield表达式中   调用时,首先要让它前进到第一个yield表达式   在发送价值之前。

您应该已经知道yield做了什么。

要完全理解生成器和测试之间的交互,它应该可以帮助您在源代码的执行流程中的每一步(用笔在一张纸上写下来)。

关于语法的小注释

另请注意yield in the manual上的警告框:

  

注意

     

如果在表达式上下文中使用yield(例如,在   在任务的右侧),你必须包围产量   带圆括号的陈述。例如,这是有效的:

     

$ data =(yield $ value);

     

但这不是,并且会导致解析错误:

     

$ data = yield $ value;

     

此语法可与结合使用   Generator :: send()方法。

答案 1 :(得分:1)

这使我困惑了几个小时,但是我设法弄清了到底是什么以及send的工作方式。重要结论是:

  • 无论您是先发送给生成器(通过send还是从中读取(通过foreachcurrent()),它将运行到第一个{{1 }}进行“初始化” ,然后在该yield上执行发送/接收操作。如果您的第一个操作正在调用yield,它将一直运行到第二个next()
  • 接下来的读取/发送操作将出口点和入口点都
  • 最后:yield依次执行发送和读取操作,无论您对结果如何处理。这意味着当您呼叫yield时,生成器将在处理您的呼叫时移至下一个send()

考虑到这一点,让我们逐步进行测试:

  1. send()仅使生成器产生,并且不运行任何操作。
  2. yield首先执行发送,然后执行读取:
    • 生成器运行,直到找到第一个$createGenerator(1)
    • 如果您要分配的话,您发送的$i1 = $generator->send(null);就是这个yield的值。
    • 生成器继续运行到下一个null,将yield添加到您的任务列表中,然后以产生的值yield将执行交还给您。
    • 在您的测试代码中,该屈服值被分配给$i1
  3. 1现在将此值发送回Generator,再次执行发送,然后将Generator推进到下一个$i1,并读取产生的值:
    • 我们仍然在$generator->send($i1); 位置,该位置以前提供了值yield。这是出口,现在成为接收发送值的入口(上面的第二个结论)。
    • 但是,您没有分配值,因此Generator继续到下一个yield($i1);
    • 下一个1不提供值,所以产生yield
    • 无论如何这都不是在测试代码中分配的,因此从外部视图来看一切都很好,但是在Generator内,发送的yield被丢弃而不是存储。 li>
  4. null现在发送1,再次导致发送和读取操作按该顺序进行,从而在此过程中推进了Generator的运行:
    • $i2 = $generator->send(null);是当前的null,以前是出口点,现在变成了入口点。 由于我们刚刚发送了$i1 = yield; //(* -> task 2),因此存储在yield 中。
    • 生成器现在会前进,将null(所以$i1)存储在null + 1中,并将其存储在任务列表中,然后产生它。
    • 这个产生的1现在存储在测试代码的$i2中。此时,先前的错误已泄漏到测试代码中。
  5. 此过程在其余测试中将继续进行,因此每一步都将发送1,并由发送它的$i2语句丢弃,然后是下一个{{1} }将存储您接下来发送的1并重复所有操作。您得到的数组将是yield

您的工作测试之所以有效,是因为它将发送的值进一步提高了一步,到达了yield,它实际上存储了您发送的内容。 @GhostGambler的Generator的工作原理相反,它将Generator内的赋值移近了null,后者实际上从测试代码中接收了值。

为说明这些内部工作原理,请考虑以下改编示例:

[1, 1, 1, 1, 1]

将创建以下输出:

yield

请注意,永远不会捕获“ out1”(第一个产生的值),因为对Generator的第一个操作是发送。因此,最后一次读取操作失败,因为Generator仅产生4个值,并且我们试图访问第五个产生的值(因为我们隐式丢弃了第一个)。

最后一个陷阱是yield,它在$createGenerator = function ($i) { $in1 = yield("out".$i++); //(* -> task 1) echo "in1: $in1"; $in2 = yield("out".$i++); //(task 1 -> *) echo "in2: $in2"; $in3 = yield("out".$i++); //(* -> task 2) echo "in3: $in3"; $in4 = yield("out".$i++); //(task 2 -> *) echo "in4: $in4"; echo "\nGenerator done with i=$i"; }; $generator = $createGenerator(1); $out = $generator->send("in1"); echo "\nout1: ";var_dump($out); $out = $generator->send("in2"); echo "\nout2: ";var_dump($out); $out = $generator->send("in3"); echo "\nout3: ";var_dump($out); $out = $generator->send("in4"); echo "\nout4: ";var_dump($out); 循环的每次迭代结束时都会隐式调用。该变量实际上与in1: in1 out1: string(4) "out2" in2: in2 out2: string(4) "out3" in3: in3 out3: string(4) "out4" in4: in4 Generator done with i=5 out4: NULL 相同:它不向Generator发送任何内容,使其运行到下一个next(),并丢弃下一个产生的值(尽管在foreach循环中,{{然后还调用1}},以提取该丢弃的值并将其存储在send(null)变量中)。这使得在yield中使用foreach相当棘手。