PHP - 生成器,send方法不遵循yield顺序

3
我希望逐步编写示例代码,说明如何将任务分离在生成器中,并将它们移动到2个或更多的生成器中,以实现它们之间的协作多任务处理。您可以在这里找到所有关于此的测试内容。
生成器在某种程度上是有逻辑性的,但我被一个无法解释为什么会这样工作的单一步骤所困扰:
生成器:
    $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 的输出的代码。因此,通过使用 send() 运行 Generator,它总是以一个输入开始,这就是为什么你不能使用 send() 获取第一个 yield 的输出,并且在最后一个 send() 之前总是会得到一个 null 返回值,这时候 Generator 进入无效状态...不幸的是,PHP手册缺乏这方面的信息...

2个回答

2

工作示例

一个生成器的工作示例,供您测试使用:

$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

进一步提示

请参阅send方法手册以获取更多信息,该手册总结了有关send如何工作的所有内容:

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

如果在调用此方法时生成器不在yield表达式处,则会先让其前进到第一个yield表达式,然后再发送该值。

您应该已经知道yield的作用。

为了充分理解生成器与您的测试之间的交互,您可以写下(用笔在纸上)源代码执行流程中的每个步骤。

语法的小注释

还请注意手册中yield的警告框

注意
如果你在表达式上下文中使用yield (例如,在赋值的右侧),你必须用括号括起来。例如,这是有效的: $data = (yield $value);
但这不是有效的,会导致解析错误: $data = yield $value;
此语法可与Generator::send()方法一起使用。

请添加关于如何将 send()yield 结合使用的一般规则! - inf3rno
我提供了一个生成器,可以满足您的测试,并提供了一些提示,应该帮助您理解在理解代码时要寻找什么。 - Ulrich Thomas Gabor
我已经理解了,但是我希望能够得到更详细的答案,以防其他人遇到同样的问题... :-)有趣的是,我们的想法完全不同,我的意思是,根据这个例子,我会在开头再添加一个yield和一个send(null),这样也可以工作...重要的是要理解,send()运行从yield的输入到下一个yield的输出的代码,并且它始终以一个输入开始,这就是为什么你不能使用send()获取第一个yield的输出。嗯,我想我会把这部分作为结论添加到问题中。 - inf3rno
你使用本地变量来存储任务,这些任务是从你的本地作用域中获取的吗?你将任务插入生成器中,只是为了再次获取它们吗?我真的不明白,在什么情况下你的生成器会有助于缓解任何与任务相关的问题。我的直觉告诉我,你想要实现一个类而不是一个生成器,因为“生成器只是‘一种简单的实现迭代器的方法,没有实现Iterator接口的类所需的开销或复杂性’”。(http://www.php.net/manual/en/language.generators.overview.php) - Ulrich Thomas Gabor
由于我不了解您的用例,我不会期望那些甚至不了解生成器工作原理的人能够理解它...如果示例更具说明性,这里的详细答案才有意义 :/ - Ulrich Thomas Gabor
你使用本地变量来存储任务,这些任务是从你的本地作用域中获取的吗?这可能只是重构代码时的短暂状态... - inf3rno

2
这让我困扰了几个小时,但是我已经找到了发生了什么以及send函数的工作原理。重要的结论如下:
  • 无论你通过 send 还是通过 foreach 或者 current() 从 Generator 中读取数据或者发送数据,它都会先运行到第一个 yield 以进行"初始化",然后在该 yield 处执行你的发送/接收操作。如果您的第一个操作是调用 next(),则会一直运行到第二个 yield
  • 每个额外的 yield 都是下一个读/写操作的退出和进入点
  • 最后:无论你对结果做什么,send() 都会按照顺序执行发送和读取操作。这意味着当你调用 send() 时,Generator 将移动到下一个 yield 并处理你的调用。
有了这些信息,我们来看看你的测试过程:
  1. $createGenerator(1) 只是生成了一个生成器,没有执行任何操作,这是预期的。
  2. $i1 = $generator->send(null); 首先执行了一次发送操作,然后进行读取操作:
    • 生成器一直运行到找到第一个 yield 为止。
    • 你发送的 null 是这个 yield 的值,如果你想要对其进行赋值的话。
    • 生成器继续运行到下一个 yield,将 $i1 添加到任务列表中,并将执行权交还,同时返回了值 1
    • 在测试代码中,这个返回值被赋值给了 $i1
  3. $generator->send($i1); 现在将这个值发送回生成器,再次执行发送操作,然后将生成器推进到下一个 yield 并读取返回值:
    • 我们仍然处于 yield($i1);,它之前提供了值 1。这是出口点,现在成为了接收发送值的入口点(参见上面的第二个结论)。
    • 但是,你没有对这个值进行赋值,所以生成器继续到下一个 yield
    • 下一个 yield 没有提供值,所以返回了 null
    • 在你的测试代码中也没有对其进行赋值,所以从外部看来一切都没问题,但是在生成器内部,发送的 1 被丢弃了而不是被存储
  4. $i2 = $generator->send(null); 现在发送了一个 null,这又一次导致了一个发送和读取操作,按顺序推进了生成器:
    • $i1 = yield; //(* -> task 2) 是当前的 yield,它之前是出口点,现在成为了接收发送值的入口点。由于我们刚刚发送了 null,所以它被存储在了 $i1
    • 生成器现在继续执行,将 null + 1(即 1)存储在 $i2 中,并将其存储在任务列表中并返回。
    • 这个返回的 1 现在被存储在测试代码的 $i2 中。此时,之前的错误已经泄露到了测试代码中。
  5. 这个过程会一直进行下去,所以每一步都会发送 1,然后被发送出去的 yield 语句将其丢弃,然后下一个 yield 将存储你发送的 null 并重复整个过程。最终得到的数组是 [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个值,而我们正在尝试访问第5个产生的值(因为我们隐式丢弃了第一个值)。
最后要注意的是next(),它在foreach循环的每次迭代结束时会被隐式调用。这与send(null)本质上是相同的:它向生成器发送空值,使其运行到下一个yield,并且丢弃下一个产生的值(虽然在foreach循环中,current()也会被调用来接收该被丢弃的值,并将其存储在as变量中)。这使得在foreach内部使用send()变得相当棘手。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接