【问题标题】:PHP - Generator, send does not follow the yield orderPHP - 生成器,发送不遵循产量顺序
【发布时间】:2014-05-30 12:21:28
【问题描述】:

我想逐步编写示例代码,如何在生成器中分离任务并将它们移动到 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));

你能用 double yield 解释一下生成器的这种奇怪行为吗?

结论:

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

【问题讨论】:

    标签: php generator yield


    【解决方案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 in the manual上的警告框:

    注意

    如果您在表达式上下文中使用 yield(例如,在 分配的右侧),您必须围绕收益 带括号的声明。例如,这是有效的:

    $data = (yield $value);

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

    $data = 产生 $value;

    此语法可以与 Generator::send() 方法。

    【讨论】:

    • 请添加关于send()yield 如何协同工作的一般规则!
    • 我提供了一个生成器,它可以完成您的测试,并提供了一些提示,这应该可以帮助您了解在理解代码时要寻找什么。
    • 我已经明白了,但是我想要一个详细的答案来解决有人遇到同样问题的案例...... :-) 有趣的是我们的想法完全不同,我的意思是我会添加一个示例更多yield在开头和更多send(null),它也可以工作......重要的是要理解,send()yield的输入运行代码,直到输出下一个yield,它总是以输入开头,这就是为什么你不能用send() 获得第一个yield 的输出。嗯,我想我会把这部分作为结论添加到问题中。
    • 您使用本地变量来存储您从本地范围获得的任务?您将任务插入生成器,只是为了让它们再次回来?我真的不明白,在哪种情况下您的生成器将有助于缓解任何与任务相关的问题。我的胃告诉我你想实现一个类而不是一个生成器作为a generator is just "an easy way to implement simple iterators without the overhead or complexity of implementing a class that implements the Iterator interface."
    • 由于我不了解您的用例,我不希望有人理解它,甚至不了解生成器的工作原理......如果示例更具说明性,这里的详细答案将是有意义的: /
    【解决方案2】:

    这让我困惑了几个小时,但我已经设法找出到底发生了什么以及send 是如何工作的。重要的结论是:

    • 无论您是先发送到(通过send)还是从(通过foreachcurrent())读取生成器,它将运行到第一个yield 以“初始化” 然后在yield 处执行您的发送/接收。如果您的第一个操作调用next(),它将一直运行到第二个yield
    • 每增加一个yield 都将是下一个读取/发送操作的退出点和入口点
    • 最后:send() 按此顺序执行发送和读取操作,无论您如何处理结果。这意味着当您呼叫send() 时,生成器将在处理您的呼叫时移动到下一个yield

    考虑到这一点,让我们来完成你的测试:

    1. $createGenerator(1) 只是生成生成器,并没有像预期的那样运行任何东西。
    2. $i1 = $generator-&gt;send(null); 先执行发送,然后执行读取:
      • 生成器一直运行,直到找到第一个 yield
      • 您发送的null 是这个yield 的值(如果您要分配它)。
      • 生成器继续运行到下一个yield,将$i1 添加到您的任务列表中,然后返回执行,生成值1
      • 在您的测试代码中,这个产生的值被分配给$i1
    3. $generator-&gt;send($i1); 现在将此值发送回生成器,再次执行发送,然后将生成器推进到下一个yield,并读取产生的值:
      • 我们仍在使用 yield($i1);,它之前提供了值 1。这是退出点,现在成为接收发送值的入口点(上面的第二个结论)。
      • 但是,您没有分配值,因此生成器会继续到下一个yield
      • 下一个yield 没有提供值,因此产生了null
      • 无论如何,这并没有在您的测试代码中分配,因此从外部看来一切都很好,但在生成器内部,发送的1 被丢弃而不是存储
    4. $i2 = $generator-&gt;send(null); 现在发送 null,这再次导致发送和读取操作,按此顺序推进生成器:
      • $i1 = yield; //(* -&gt; task 2) 是当前的yield,以前是出口点,现在成为入口点。 由于我们刚刚发送了null,所以这就是存储在$i1中的内容
      • 生成器现在前进,将null + 1(即1)存储在$i2 中,将其存储在您的任务列表中,然后生成它。
      • 产生的1 现在存储在您的测试代码的$i2 中。至此,之前的错误已经泄露到了测试代码中。
    5. 这会在接下来的测试中继续,所以每个步骤都会发送1,这将被发送它的yield 语句丢弃,然后下一个yield 将存储@987654364 @您接下来发送并重复所有内容。您的结果数组将是 [1, 1, 1, 1, 1]

    您的工作测试有效,因为它将发送的值更进一步,到实际存储您发送的内容的yield。 @GhostGambler 的生成器出于相反的原因工作,它将生成器中的分配移近一步,靠近实际从测试代码接收值的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
    

    请注意,“out1”(第一个产生的值)永远不会被捕获,因为生成器上的第一个操作是发送。因此,最后一次读取操作失败了,因为生成器只产生了 4 个值,而我们正在尝试访问第五个产生的值(因为我们隐式丢弃了第一个值)。

    最后一个问题是next(),它在foreach 循环的每次迭代结束时被隐式调用。那一个本质上与send(null) 相同:它不向生成器发送任何内容,使其运行到下一个yield,并丢弃下一个产生的值(尽管在foreach 循环中,current() 也被调用到拾取丢弃的值并将其存储在as 变量中)。这使得在foreach 中使用send() 变得相当棘手。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-03-28
      • 1970-01-01
      • 1970-01-01
      • 2011-05-06
      • 1970-01-01
      • 2017-07-01
      • 2017-02-16
      相关资源
      最近更新 更多