PHP的yield是个什么玩意(一)

其实,我并不是因为迭代或者生成器或者研究 PHP 手册才认识的 yield,要不是协程,我到现在也不知道 PHP 中还有 yield 这么个鬼东西。

人家这个东西是从 PHP5.5 就开始引入了,官方名称叫做生成器

你要说为什么 5.5 年代的东西,现在才拿出来。

我还想问你哟,PHP 5.3 就有了的 namespace 为毛到最近这几年才开始正式投产。

那么,问题来了,这东西到底是有何用?

先来感受一个问题,给你 100Kb 的内存(是的,你没有看错,就是 100Kb),然后让你迭代输出一个从 1 开始一直到 10000 的数组,步进为 1。

愈先迭代数组,必先创造数组。

所以,脑门一拍,代码一坨如下:

<?php
$start_mem = memory_get_usage();
$arr = range( 1, 10000 );
foreach( $arr as $item ){
  //echo $item.',';
}
$end_mem = memory_get_usage();
echo " use mem : ". ( $end_mem - $start_mem ) .'bytes'.PHP_EOL;

一顿操作猛如虎,运行一下成绩 1-5,你们感受一下:

$ php test.php
use mem:528440bytes

528440bytes,约莫就是528Kb,几乎是 100Kb 的五倍了,妈的这日子没法过了。

毕竟你们也知道,最近内存价格确实贵,国家也在号召低碳节能减排,你多耗费5倍内存,就意味着多排放5倍的二氧化碳,就意味着要为多用的内存多花钱贡献给棒子... ...你想想,那可是棒子。

人都是被逼出来的,于是 yield 可以来救场了,大概代码如下,注意看操作:

<?php

$start_mem = memory_get_usage();

function yield_range($start, $end) {
    while ($start <= $end) {
        yield $start;
        $start++;
    }
}

foreach(yield_range(1, 10000) as $item) {
    // echo $item . ',';
}

$end_mem = memory_get_usage();

echo 'use mem:' . ($end_mem - $start_mem) . 'bytes' . PHP_EOL;

运行一下,你们感受一下:

$ php test.php
use mem:0bytes

首先,我们观察一下 yield_range 这个函数跟普通函数不一样的地方,就是普通函数往往都是使用 return 来返回结果,而这个中则是 yield。

其次是普通函数中 return 只能返回一次,这个 yield 能返回好多次。

那么,我们来分析一波儿这个神奇的 yield_range 函数。这个 yield 关键字到底返回的是什么?

我们简单看一下:

<?php

function yield_range($start, $end) {
    while($start <= $end) {
        yield $start;
        $start++;
    }
}

$rs = yield_range(1, 100);
var_dump($rs);
/**
object(Generator)#1 (0) {
}
 */

yield 返回的是一个叫做 Generator(中文名就是生成器)的 object 对象,而这个生成器是实现了 Iterator 接口(至于Iterator接口,你们去PHP手册上搜索吧)。

所以,既然实现了 Iterator 接口(也正是因为如此,这个东西可以使用 foreach 进行迭代,明白了吧?),

所以可以有如下代码:

<?php

function yield_range($start, $end) {
    while ($start <= $end) {
        yield $start;
        $start++;
    }
}

$generator = yield_range(1, 100);

// valid()、current()、next() 都是 Iterator接口中的方法
while ($generator->valid()) {
    echo $generator->current() . PHP_EOL;
    $generator->next();
}

运行结果如下所示:

$ php test.php
1
2
3
4
5
6
7
8
9
10

重点来了:这个 yield_range 函数似乎能够记住它上一次运行到哪儿了,上一次运行的结果是什么,然后紧接着在下一次运行的时候继续从上次终止的地方继续开始。

这不是普通的PHP函数可以做得到的!

我们知道,操作系统在调度进程的时候,会触发一个叫做 “进程上下文切换” 的概念。

比如 CPU 从进程 A 调度给进程 B 了,那么当再次从进程 B 调度给进程 A 的时候,当初进程 A 运行到哪儿了、临时的数据结果是什么都是需要被还原的,不然,一切都要从头,那就要出大问题了。

然而,这个 yield 关键字,似乎在用户态(非系统内核级)就可以实现这个概念。

所以说,用 yield 搞迭代,怕是真的很没出息的一件事,它能做的太多。

紧接着,我们需要认识一个生成器对象的一个方法,叫做 send,简单看下下面这坨代码:

<?php

function yield_range($start, $end) {
    while ($start <= $end) {
        $ret = yield $start;
        $start++;
        echo 'yield receive:' . $ret . PHP_EOL;
    }
}

$generator = yield_range(1, 10);

$generator->send( $generator->current() * 10 );

运行结果如下所示:

$ php test.php
yield receive:10

send 方法可以修改 yield 的返回值,但是,你也不能想当然,比如下面这坨代码,你们以为运行结果是什么样呢?

<?php

function yield_range($start, $end) {
    while ($start <= $end) {
        $ret = yield $start;
        $start++;
        echo "yield receive: " . $ret . PHP_EOL;
    }
}

$generator = yield_range(1, 10);

foreach($generator as $item) {
    $generator->send($generator->current() * 10);
}

本来以为运行结果是类似于这样的:

$ php test.php
yield receive : 10
yield receive : 20
yield receive : 30
yield receive : 40
yield receive : 50
yield receive : 60
yield receive : 70
yield receive : 80
yield receive : 90
yield receive : 100

然而,唯物主义告诉我们:实践是检验真理的唯一标准!

结果是打脸的,你们感受一下:

$ php test.php
yield receive: 10
yield receive: 
yield receive: 30
yield receive: 
yield receive: 50
yield receive: 
yield receive: 70
yield receive: 
yield receive: 90
yield receive: 

原因是什么呢?

原因是当你在外部向 yield 发送 send 的时候,会自动触发一次 next,自己动手试下吧。

评论