php的yield和Generator 及其应用
查看laravel源代码时看到如下代码
vendor\laravel\framework\src\Illuminate\Container\Container.php
public function tagged($tag)
{
if (! isset($this->tags[$tag])) {
return [];
}
return new RewindableGenerator(function () use ($tag) {
foreach ($this->tags[$tag] as $abstract) {
yield $this->make($abstract);
}
}, count($this->tags[$tag]));
}
什么是yield呢?
官方解释如下:
生成器语法
当一个生成器被调用的时候,它返回一个可以被遍历的对象,当你遍历这个对象的时候(例如通过一个foreach)将会在每次需要值的时候调用生成器函数,并在产生一个值之后保存生成器的状态,这样它就可以在需要产生下一个值的时候恢复调用状态。
一旦不再需要产生更多的值,生成器函数可以简单退出,而调用生成器的代码还可以继续执行,就像一个数组已经被遍历完了。
生成器函数的核心是yield关键字。它最简单的调用形式看起来像一个return申明,不同之处在于普通return会返回值并终止函数的执行,而yield会返回一个值给循环调用此生成器的代码并且只是暂停执行生成器函数。
看起来很晦涩,和return有点像?
所以 我们先写个最简单的测试,代码如下:
<?php
function gen() {
yield 1;
}
var_export(gen());
/*
* return
*
Generator::__set_state(array(
))*/
所以什么是Generator 生成器?
Generator提供了一种方便的实现简单的Iterator(迭代器)的方式,使用Generator实现Iterator不需要创建一个类来继承
Iterator接口。
所以什么是Iterator 迭代器
对象
进行遍历时 需要实现Iterator接口 提供的5个方法:
Iterator extends Traversable {
/* Methods */
abstract public mixed current ( void ) //返回当前位置的元素
abstract public scalar key ( void ) //返回当前元素对应的key
abstract public void next ( void ) //移到指向下一个元素的位置
abstract public void rewind ( void ) //倒回到指向第一个元素的位置
abstract public boolean valid ( void ) //判断当前位置是否有效
}
从上面代码看到 Iterator 继承了 Traversable, 那Traversable是什么
Traversable是一个空接口,一个标志,标志一个非数组的变量是否可以通过foreach遍历
通常可以用下面的代码来判断一个变量是否可以通过foreach进行遍历
<?php
if( !is_array( $items ) && !$items instanceof Traversable s)
//Throw exception here
?>
回到最开始,什么是Generator
先看一个例子
function xrange($start, $end, $step = 1) {
for ($i = $start; $i <= $end; $i += $step) {
yield $i;
}
}
foreach (xrange(1, 10) as $num) {
echo $num, "\n";
}
//return
/*
1
2
3
4
5
6
7
8
9
10
*/
以上代码最终效果等同于
foreach (range(1, 10) as $num) {
echo $num, "\n";
}
//return
/*
1
2
3
4
5
6
7
8
9
10
*/
区别呢?? 两者不同的是range会一次性返回包含所有元素的数组,而xrange是遍历过程中迭代一次返回一个,它之所以可以这么做是因为调用xrange返回的是一个Generator对象,我们再在上面的代码添加几行代码:
function xrange($start, $end, $step = 1) {
for ($i = $start; $i <= $end; $i += $step) {
yield $i;
}
}
$range = xrange(1, 10);
foreach ($range as $num) {
echo $num, "\n";
}
var_dump($range); // object(Generator)#1
var_dump($range instanceof Iterator); // bool(true)
可以看出 xrange
返回的是一个 Generator对象,继承了Iterator
看文档 ,Generator对象定义如下
Generator implements Iterator {
/* Methods */
public mixed current ( void )
public mixed key ( void )
public void next ( void )
public void rewind ( void )
public mixed send ( mixed $value )
public mixed throw ( Exception $exception )
public bool valid ( void )
public void __wakeup ( void )
}
比Iterator多的三个方法
_wakeup是一个魔术方法,用于序列化,Generator实现这个方法是为了防止序列化
throw 不太用管
send 用于向生成器传入参数,后面介绍
Generator 还有一个特性,它是一个类,但是它并不能通过new方法进行实例化
测试代码如下
$g = new Generator();
/*
PHP Fatal error: Uncaught Error: The "Generator" class is reserved for internal use and cannot be manually instantiated in ****
意思就是 Generator类只给内部使用 不能人工初始化
*/
回到yield,知道了Generator 内部实现的方法,可以测试如下代码
<?php
function gen()
{
yield 1;
}
$g = gen();
var_export($g->valid()); //true
echo "\r\n";
var_export($g->current());; //1
echo "\r\n";
$g->next();
var_export($g->valid());; //false
echo "\r\n";
var_export($g->current());; //NULL
echo "\r\n";
/*
true
1
false
NULL
*/
变量$g是生成器对象,继承了Iterator,我们可以不通过foreach,手动调用Iterator的方法,在next()方法被调用后,对象已经被迭代完毕,所以后续返回空,符合预期;
同理下面代码也不难理解:
<?php
function gen() {
yield 1;
yield 2;
yield 3;
}
$g = gen();
var_export($g->valid());
var_export( $g->current());
echo "\n";
echo $g->next();
var_export( $g->valid());
var_export( $g->current());
echo "\n";
echo $g->next();
var_export( $g->valid());
var_export( $g->current());
echo "\n";
echo $g->next();
var_export( $g->valid());
var_export( $g->current());
echo "\n";
/*
true1
true2
true3
falseNULL
*/
看代码的最上面部分,就能很直观的看出yield和return在表现形式上的区别了,return只返回一次,而且值固定,yield随着不断迭代,可以返回后续的值。
和return还有啥区别呢? yield只能用在函数中,代码如下
<?php
return "aaa";
<?php
yield "aaa";
/*
PHP Fatal error: The "yield" expression can only be used inside a function in /vagrant/testx/t_yield.php on line 2
*/
一个小问题:foreach返回的是$key => v a l u e 形 式 上 面 的 例 子 都 只 返 回 了 value 形式 上面的例子都只返回了 value形式上面的例子都只返回了value,能不能也返回$key呢
当然可以,例子如下:
<?php
function gen() {
yield 1 => "a";
yield 2 => "b";
yield 3 => "c";
}
$g = gen();
var_export($g->key());
var_export( $g->current());
echo "\n";
echo $g->next();
var_export( $g->key());
var_export( $g->current());
echo "\n";
echo $g->next();
var_export( $g->key());
var_export( $g->current());
echo "\n";
echo $g->next();
var_export( $g->key());
var_export( $g->current());
echo "\n";
/*
1'a'
2'b'
3'c'
NULLNULL
*/
另一个小问题:在生成器中加入另外逻辑,他们的会怎么执行呢
<?php
function gen() {
yield 'yield1';
echo "round1\r\n";
yield 'yield2';
echo "round2\r\n";
}
$g = gen();
var_export( $g->current()); //yield1
echo "\n";
$g->next(); //round1
var_export( $g->current()); //yield2
echo "\n";
$g->next(); //round2
var_export( $g->current()); //NULL
echo "\n";
/*
'yield1'
round1
'yield2'
round2
NULL
*/
可以看出,在调用next()方法时,程序会向下找下一个yield关键字,在下个yield位置之前的代码都会被执行
上面说的send(mixed v a l u e ) , 方 法 是 干 啥 的 呢 , value),方法是干啥的呢, value),方法是干啥的呢,value值是什么呢
首先 yield也可以用于表达式的上下文中,例如用于赋值语句的右侧,如下(根据上面的实验,这块代码必须写在函数中):
$data = (yield $value);
可以理解成吧一个表达式赋值给了$data,所以需要用括号括起来
对比以下代码
function gen() {
$ret = (yield 'yield1');
var_dump($ret);
$ret = (yield 'yield2');
var_dump($ret);
}
$g = gen();
var_dump($g->current());
$g->next();
var_dump($g->current());
/*
/vagrant/testx/t_yield.php:9:
string(6) "yield1"
/vagrant/testx/t_yield.php:4:
NULL
/vagrant/testx/t_yield.php:11:
string(6) "yield2"
*/
function gen() {
$ret = (yield 'yield1');
var_dump($ret);
$ret = (yield 'yield2');
var_dump($ret);
}
$g = gen();
var_dump($g->current());
var_dump($g->send('ret1'));
/*
/vagrant/testx/t_yield.php:21:
string(6) "yield1"
/vagrant/testx/t_yield.php:16:
string(4) "ret1"
/vagrant/testx/t_yield.php:22:
string(6) "yield2"
*/
对比输出不难发现,send($value),实现了向Generator对象中传入参数的功能,作为当前yield表达式的结果,然后执行了next+current方法,
Generator能用来干啥
1.很显然,要返回一个大数组,用生成器更节省内存,还是上面的代码,因为xrang()方法在被调用的时候才实时生成要返回的数,而range(),直接生成好了数组
function xrange($start, $end, $step = 1) {
for ($i = $start; $i <= $end; $i += $step) {
yield $i;
}
}
foreach (xrange(1, 10) as $num) {
echo $num, "\n";
}
foreach (range(1, 10) as $num) {
echo $num, "\n";
}
2.多线程,
遍历Generator对象的每次迭代都只会执行前一次yield语句之后的代码,而且碰到yield语句就会返回一个值,相当于从generator函数中返回,这有点像挂起一个进程(线程)的执行(yield在很多语言中就是用于挂起进程(线程)),然后又启动它继续执行,周而复始直到进程(线程)执行中止,这也是为什么Generator可以用于实现协程的原因。