生成器 yield理解

yield

生成器是PHP 5.5.0才引入的功能

如要深入,可以看鸟哥的文摘 协程实现多任务调度

生成器优点

  • 生成器会对PHP应用的性能有非常大的影响
  • PHP代码运行时节省大量的内存
  • 比较适合计算大量的数据

那么,这些神奇的功能究竟是如何做到的?我们先来举个例子

概念引入

首先,来看一个简单的PHP函数:

function createRange($number){
    $data = [];
    for($i=0;$i<$number;$i++){
        $data[] = time();
    }
    return $data;
}

这里的代码也非常简单:

我们创建一个函数, 函数内包含一个 for 循环,我们循环的把当前时间放到$data里面, for循环执行完毕,把 $data 返回出去。

我们再写一个函数,把这个函数的返回值循环打印出来:

$result = createRange(5);
foreach($result as $value){
    sleep(1);//这里停顿1秒,我们后续有用
    echo $value.'<br />';
}

运行结果:

1540537809
1540537809
1540537809
1540537809
1540537809

犹豫数量小,sleep(1) 效果你们看不出来

思考一个问题

我们注意到,在调用函数 createRange 的时候给 $number 的传值是10,一个很小的数字。假设,现在传递一个值10000000(1000万)。

那么,在函数 createRange 里面,for循环就需要执行1000万次。且有1000万个值被放到 $data 里面,而$data数组在是被放在内存内。所以,在调用函数时候会占用大量内存。

这里,生成器就可以大显身手了。

生成器的使用

创建生成器

我们直接修改代码,你们注意观察:

function createRange($number){
    for($i=0;$i<$number;$i++){
        yield time();
    }
}

看下这段和刚刚很像的代码,我们删除了数组 $data ,而且也没有返回任何内容,而是在 time() 之前使用了一个关键字 yield

使用生成器

我们再运行一下第二段代码:

$result = createRange(10); // 这里调用上面我们创建的函数
foreach($result as $value){
    sleep(1);
    echo $value.'<br />';
}

运行结果:

1540538241
1540538242
1540538243
1540538244
1540538245

我们奇迹般的发现了,输出的值和第一次没有使用生成器的不一样。这里的值(时间戳)中间间隔了1秒。

这里的间隔一秒其实就是 sleep(1) 造成的后果。但是为什么第一次没有间隔?那是因为: 未使用生成器时: createRange 函数内的 for 循环结果被很快放到 $data 中,并且立即返回。所以, foreach 循环的是一个固定的数组。 使用生成器时: createRange 的值不是一次性快速生成,而是依赖于 foreach 循环。 foreach 循环一次, for 执行一次。

到这里,你应该对生成器有点儿头绪。

深入理解生成器

下面我们来对于刚刚的代码进行剖析。

function createRange($number){
    for($i=0;$i<$number;$i++){
        yield time();
    }
}

$result = createRange(10); // 这里调用上面我们创建的函数
foreach($result as $value){
    sleep(1);
    echo $value.'<br />';
}

我们来还原一下代码执行过程。

  1. 首先调用 createRange 函数,传入参数10,但是 for 值执行了一次然后停止了,并且告诉 foreach 第一次循环可以用的值。
  2. foreach 开始对 $result 循环,进来首先 sleep(1) ,然后开始使用 for 给的一个值执行输出。
  3. foreach 准备第二次循环,开始第二次循环之前,它向 for 循环又请求了一次。
  4. for 循环于是又执行了一次,将生成的时间戳告诉 foreach .
  5. foreach 拿到第二个值,并且输出。由于 foreach 中 sleep(1) ,所以, for 循环延迟了1秒生成当前时间

所以,整个代码执行中,始终只有一个记录值参与循环,内存中也只有一条信息。

无论开始传入的 $number 有多大,由于并不会立即生成所有结果集,所以内存始终是一条循环的值。

继续举例深入理解

function gen() {
    $ret = (yield 'yield1');
    var_dump($ret);
    $ret = (yield 'yield2');
    var_dump($ret);
}
 
$gen = gen();
var_dump($gen->current());    // string(6) "yield1"
var_dump($gen->send('ret1')); // string(4) "ret1"   (the first var_dump in gen)
                              // string(6) "yield2" (the var_dump of the ->send() return value)
var_dump($gen->send('ret2')); // string(4) "ret2"   (again from within gen)
                              // NULL               (the return value of ->send())

上面的代码首先是调用函数gen生成一个Generator对象,然后调用这个对象的current方法返回第一个值,显然它是第一个yield语句的返回值,也就是'yield1',这个时候gen函数的执行就会被中止,接着执行var_dump($g->send('ret1'));。

调用$g->send('ret1'),传入参数为字符串'ret1',按照上面的说明,它会赋值给第一个yield表达式,也就是(yield 'yield1')中的yield(注意:这个时候不包括'yield1'),它的值为'ret1',然后会赋值给$ret,所以第二个输出'ret1'就是gen函数中的第一个var_dump输出的。此时对Generator对象的迭代会恢复继续执行,实际上就是调用了一次next函数,它会执行到下一个yield语句:yield 'yield2',这个语句会返回'yield2',它会作为$g->send('ret1')的返回值,所以函数外第二个var_dump会输出'yield2'。

最后再次调用send函数,这次传入的参数为字符串'ret2',跟上面一样,Generator对象当前位置的元素是在gen函数的第二个yield上,所以’ret2'会被传递给第二个yield表达式,也就是作为(yield 'yield2')中的yield的值,并且会被赋值给$ret变量,然后gen函数恢复执行,它会执行gen函数中的最后一个var_dump,此时对Generator对象$g的遍历也结束了,第二个send函数的返回值为NULL,这也是函数外的最后一个var_dump的输出。

读了这么一段分析以后,你现在最大的困惑是什么呢?

我最大的困惑是为什么同一个yied关键字,它既是语句,又是表达式,而且这两种情况是同时存在的:

对于所有在generator函数中出现的yield,首先它都是语句,而跟在yield后面的任何表达式的值将作为调用generator函数的返回值,如果yield后面没有任何表达式(变量、常量都是表达式),那么它会返回NULL,这一点跟return语句一致。

  • yield也是表达式,它的值就是send函数传过来的值(相当于一个特殊变量,只不过赋值是通过send函数进行的)。只要调用send方法,并且Generator对象的迭代并未终结,那么当前位置的

  • yield就会得到send方法传递过来的值,这跟generator函数有没有把这个值赋值给某个变量没有任何关系。

从上面两点我们就可以看出,任何时候yield关键词都即是语句——可以为generator函数返回值,也是表达式——可以接收Generator对象发过来的值。

但有点疑问就是:

在执行完gen中的var_dump之后,generator应该终止啊。但是,为什么却又恢复了,继续执行下一条yield语句呢。 我猜是因为当yield作为表达式的时候,generator并没有进行迭代。只有yield被当做了语句执行之后,generator才会终止吧。

举例补充说明这个终止

function gen() {
	for($i=1;$i<=100;$i++) {
		$cmd = (yield $i);
		if($cmd=='stop') {
			return;
		}
	}
}
$gen = gen();
$i=0;
foreach($gen as $item) {
	echo $item."\n";
	if($i>=10) {
		$gen->send('stop');
	}
	$i++;
}

这是个很好地例子:

  1. yield作为语句(类似return语句),会返回$i给调用者。
  2. yield作为表达式。获取send函数传递值,赋值给$cmd。
  3. 实现Generator对象和generator函数的通信。这个很重要。应该能实现很多generator的交互.

概念理解

到这里,你应该已经大概理解什么是生成器了。下面我们来说下生成器原理。

首先明确一个概念:生成器yield关键字不是返回值,他的专业术语叫产出值,只是生成一个值

那么代码中 foreach 循环的是什么?其实是PHP在使用生成器的时候,会返回一个 Generator 类的对象。 foreach 可以对该对象进行迭代,每一次迭代,PHP会通过 Generator 实例计算出下一次需要迭代的值。这样 foreach 就知道下一次需要迭代的值了。

而且,在运行中 for 循环执行后,会立即停止。等待 foreach 下次循环时候再次和 for 索要下次的值的时候,循环才会再执行一次,然后立即再次停止。直到不满足条件不执行结束。

迭代生成数组: 键=>值

如,

function createRange($number){
    for($i=0;$i<$number;$i++){
        yield $i => time();
    }
}

实际开发应用

  • 读取超大文件 PHP开发很多时候都要读取大文件,比如csv文件、text文件,或者一些日志文件。这些文件如果很大,比如5个G。这时,直接一次性把所有的内容读取到内存中计算不太现实。

这里生成器就可以派上用场啦。简单看个例子:读取text文件

<?php
header("content-type:text/html;charset=utf-8");
function readTxt()
{
    # code...
    $handle = fopen("./test.txt", 'rb');

    while (feof($handle)===false) {
        # code...
        yield fgets($handle);
    }

    fclose($handle);
}

foreach (readTxt() as $key => $value) {
    # code...
    echo $value.'<br />';
}

使用生成器读取文件,第一次读取了第一行,第二次读取了第二行,以此类推,每次被加载到内存中的文字只有一行,大大的减小了内存的使用。

这样,即使读取上G的文本也不用担心,完全可以像读取很小文件一样编写代码。

猜你喜欢

转载自my.oschina.net/u/3683692/blog/2252991
今日推荐