Generator官方文档介绍
(PHP 5 >= 5.5.0, PHP 7)
生成器提供了一种更容易的方法来实现简单的对象迭代,相比较定义类实现 Iterator 接口的方式(这种叫迭代器),性能开销和复杂性大大降低。
生成器允许你在 foreach 代码块中写代码来迭代一组数据而不需要在内存中创建一个数组, 那会使你的内存达到上限,或者会占据可观的处理时间。相反,你可以写一个生成器函数,就像一个普通的自定义函数一样, 和普通函数只返回一次不同的是, 生成器可以根据需要 yield 多次,以便生成需要迭代的值。
一个生成器函数看起来像一个普通的函数,不同的是普通函数返回一个值,而一个生成器可以yield生成许多它所需要的值。
当一个生成器被调用的时候,它返回一个可以被遍历的对象。当你遍历这个对象的时候(例如通过一个foreach循环),PHP 将会在每次需要值的时候调用生成器函数,并在产生一个值之后保存生成器的状态,这样它就可以在需要产生下一个值的时候恢复调用状态。
一旦不再需要产生更多的值,生成器函数可以简单退出,而调用生成器的代码还可以继续执行,就像一个数组已经被遍历完了。
普通函数只能返回一个值,生成器函数可以yield返回多次值,并且只在需要值的时候被调用,可以节省内存使用空间。
实际上生成器生成的正是一个迭代器对象实例,该迭代器对象继承了 Iterator 接口,同时也包含了生成器对象自有的接口,具体可以参考 Generator类的定义以及语法参考。
实现了Iterator接口的Generator类示例(便于了解生成器内部结构)
class Generator implements Iterator {
//返回当前产生的值
public mixed current ( void )
//返回当前产生的键
public mixed key ( void )
//生成器继续执行
public void next ( void )
//重置迭代器,如果迭代已经开始了,这里会抛出一个异常。
public void rewind ( void )
//向生成器中传入一个值,当前yield接收值,然后继续执行下一个yield
public mixed send ( mixed $value )
//向生成器中抛入一个异常
public void throw ( Exception $exception )
//检查迭代器是否被关闭,已被关闭返回 FALSE,否则返回 TRUE
public bool valid ( void )
//序列化回调
public void __wakeup ( void )
//返回generator函数的返回值,PHP version 7+
public mixed getReturn ( void )
}
yield关键字
生成器函数的核心是yield关键字。它最简单的调用形式看起来像一个return申明,不同之处在于普通return会返回值并终止函数的执行,而yield会返回一个值给循环调用此生成器的代码并且只是暂停执行生成器函数。
yield关键字只能在函数中使用,否则会报Fatal error: The “yield” expression can only be used inside a function,凡是使用了yield关键字的函数都会返回一个Generator对象。每次代码执行到yield语句都会中断执行,返回yield语句中表达式的值给Generator对象,继续迭代Generator对象时,yield后面的代码会接着执行,直到所有yield语句全部执行完毕或者有return语句,这个renturn语句只能返回null,否则会编译错误。
1、一个简单的生成值的例子
function gen_one_to_three() {
for ($i = 1; $i <= 3; $i++) {
//注意变量$i的值在不同的yield之间是保持传递的。
yield $i;
}
}
$generator = gen_one_to_three();
foreach ($generator as $value) {
echo "$value\n";
}
运行结果截图
2、指定键名来生成值
PHP的数组支持关联键值对数组,生成器也一样支持。所以除了生成简单的值,你也可以在生成值的时候指定键名。如下所示,生成一个键值对与定义一个关联数组十分相似。
/*
* 下面每一行是用分号分割的字段组合,第一个字段将被用作键名。
*/
$input = <<<'EOF'
1;PHP;Likes dollar signs
2;Python;Likes whitespace
3;Ruby;Likes blocks
EOF;
function input_parser($input) {
foreach (explode("\n", $input) as $line) {
$fields = explode(';', $line);
$id = array_shift($fields);
yield $id => $fields;
}
}
foreach (input_parser($input) as $id => $fields) {
echo "$id:" . "<br/>";
echo " $fields[0]" . "<br/>";
echo " $fields[1]" . "<br/>";
}
运行结果截图
3、代码运行讲解
function test()
{
$a = (yield 111);
var_dump('test()->$a:'.$a);echo "<br/>";
$b = (yield 222);
var_dump('test()->$b:'.$b);echo "<br/>";
}
/**
* 第一次调用test函数,执行到yield,中断执行,生成一个Generator对象(Generator对象当前值为111)
*/
$gen = test();
/**
* $gen->current()读取Generator对象当前值111进行返回,var_dump打印结果是int(111)
*/
var_dump($gen->current());echo "<br/>";
/**
* $gen->send(333)唤醒$gen的迭代执行,Generator对象当前yield($a)接收值333,然后从上一次中断的位置往下开始执行(上一次中断的位置$a = (yield 111),所以这次应该执行var_dump('test()->$a:'.$a);echo "<br/>";)
* var_dump打印结果是 string(14) "test()->$a:333";
* 直到执行到$b = yield 222,再次中断执行,生成一个Generator对象(Generator对象当前值为111),并且把Generator对象当前值222进行返回
* var_dump打印结果是int(222)
*/
var_dump($gen->send(333));echo "<br/>";
/**
* $gen->send(444)唤醒$gen的迭代执行,Generator对象当前yield($b)接收值444,然后从从上一次中断的位置往下开始执行(var_dump('test()->$b:'.$b);echo "<br/>";)
* var_dump打印结果是 string(14) "test()->$b:444"
* 再往下没有yield关键字,迭代终止,没有再生成Generator对象,$gen->send(444)值为NULL
* var_dump打印结果是NULL
*/
var_dump($gen->send(444));echo "<br/>";
/**
* 迭代已经终止,$gen->next()不能唤醒$gen的迭代执行,所以$gen->next()值为NULL
* var_dump打印结果是NULL
*/
var_dump($gen->next());
运行结果截图
适用场景:
- 协程(这里不讨论)
- CSV、TXT、日志的大文件数据处理场景,使用yield迭代处理不用一次性加载到内存中,可以减少内存占用
TXT文件截图
读取TXT文件代码示例
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 />';
}
运行结果截图