理解 PHP 的 Generator
Generator 的中文名叫做生成器,于 PHP5.5 中引入。提供了一种更容易的方法来实现简单的对象迭代。相比于定义类实现 Iterator 接口的方式,大大降低了性能开销与复杂性。
Iterator 是什么
如果要搞明白 Generator,就需要先理解 Iterator接口。 Iterator 是 PHP 的迭代器。我们日常中使用 foreach 遍历时,就隐式调用了迭代器。PHP 官方手册中介绍:Iterator 可在内部迭代自己的外部迭代器或类的接口。Iterator 中有五个方法:
<?php
interface Iterator extends Traversable {
/* Methods */
public current(): mixed // 返回当前元素的位置
public key(): mixed // 返回当前元素的 key
public next(): void // 移动到下一个元素
public rewind(): void // 重置到第一个元素的位置
public valid(): bool // 检查当前位置是否有效
}
一个简单的迭代器范例:
<?php
class MyIterator implements Iterator
{
private $position = 0;
private $arr = [];
public function __construct($arr) {
$this->arr = $arr;
}
public function current()
{
echo "调用了current\n";
return $this->arr[$this->position];
}
public function next()
{
echo "调用了next\n";
++$this->position;
}
public function key(): int
{
echo "调用了key\n";
return $this->position;
}
public function valid(): bool
{
echo "调用了valid\n";
return isset($this->arr[$this->position]);
}
public function rewind()
{
echo "调用了rewind\n";
$this->position = 0;
}
}
$arr = [
"我是第一个",
"我是第二个",
"我是第三个",
];
$it = new myIterator($arr);
foreach ($it as $k => $v) {
var_dump($k, $v);
echo "\n";
以上程序会输出:
调用了rewind
调用了valid
调用了current
调用了valid
调用了key
int(0)
string(15) "我是第一个"
调用了next
调用了valid
调用了current
调用了valid
调用了next
调用了valid
调用了current
调用了key
int(2)
string(15) "我是第三个"
调用了next
调用了valid
Generator 对象
简介
Generator 的中文名称叫做“生成器”。Generator 不能实例化,也不能继承。Generator 提供了一种更容易的方法实现迭代器,且性能开销相比于 Iterator 更小。生成器允许你在 foreach 代码块中写代码来迭代一组数据而不需要在内存中创建一个数组。这在处理大量的数据好处显而易见,可以节省内存且缩短运行时间。Generator 的实现不需要像 Iterator 那样复杂,只需要在普通的函数体中加入 yield 关键词即可实现。与普通函数不同的是,普通函数只能 return 一次数据,而 yield 可以返回多次数据,且返回数据后还能回到原来的位置,继续往下执行函数体内的程序。
<?php
final class Generator implements Iterator {
/* Methods */
public current(): mixed
public getReturn(): mixed
public key(): mixed
public next(): void
public rewind(): void
public send(mixed $value): mixed
public throw(Throwable $exception): mixed
public valid(): bool
public __wakeup(): void
}
Generator 从字面意思中比较难以理解,下面会介绍语法和列举一个简单的例子来帮助更好的理解 Generator。
语法
yield 是实现 Generator 的关键,yield 只能在函数中使用,当 yield 在函数体外使用时,会抛出一个致命错误:
Fatal error: The "yield" expression can only be used inside a function
任何包含 yield 的函数都是一个生成器函数。当一个生成器被调用的时候,它返回一个可以被遍历的对象。生成器函数会在每次需要值产生一个值返回,且保存当前生成器的状态。当需要产生下一个值时,就恢复调用状态。当不在需要更多的值时,生成器可以简单的退出,而调用生成器的代码还可以继续执行,就像一个数组已经被遍历完成了。不过,与迭代器相比,生成器只能是一个向前的迭代器,一旦开始就无法后退。一个简单的例子:
<?php
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";
}
以上程序会输出:
1
2
3
使用 yield 实现 range:
function xrange($start, $limit, $step = 1) {
if ($start <= $limit) {
if ($step <= 0) {
throw new LogicException("step需要为正数");
}
for ($i = $start; $i <= $limit; $i += $step) {
yield $i;
}
} else {
if ($step >= 0) {
throw new LogicException("step需要为负数");
}
for ($i = $start; $i >= $limit; $i += $step) {
yield $i;
}
}
}
xrange 这个方法如同 php 内置的 range 方法,不同的是 range 会一次性返回包含所有元素的数组,而 xrange 是遍历过程中迭代一次返回一个,它之所以可以这么做是因为调用 xrange 返回的是一个Generator对象,我们再在上面的代码添加几行代码:
$range = xrange(1, 1000000);
var_dump($range); // object(Generator)#1
var_dump($range instanceof Iterator); // bool(true)
yield 指定键名生成值
PHP的数组支持关联键值对数组,生成器也一样支持。所以除了生成简单的值,你也可以在生成值的时候指定键名。
<?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:\n";
echo " $fields[0]\n";
echo " $fields[1]\n";
}
以上程序输出:
1:
PHP
Likes dollar signs
2:
Python
Likes whitespace
3:
Ruby
Likes blocks
总结
Generator 内部实现了 Iterator,可迭代的值就时 yield 可以返回的值的集合。当遍历 Generator 时,就开始执行 yield 之前的代码,碰到 yield 就像普通函数一样返回值,就好像挂起了当前的状态,当下一次继续执行时,就能找回之前的状态继续执行。就这样周而复始的运行,直到当前进程终止。这一幕是不是有点熟悉呢,没错,这就是协程啦。Generator 是可以实现协程的。
yeild还可以生成 NULL 值,引用来生成值,关于更多 yield 的用法,可以参考 yield的文档 。
使用生成器实现斐波那契数列
<?php
function fibonacci($len) {
$i = 1; // 数列序号
$pre = 0; // 记录上一轮的值,初始值f(1)的上一轮是f(0) = 0
$v = 1; // 记录当前的值,初始值是f(1) = 1
while ($i <= $len) {
yield $v;
$tmp = $v; // 记录本轮值
$v += $pre; // 将本轮值和上一轮值相加 yield 出去
$pre = $tmp; // 本轮值交给下一次循环的上一轮
$i++;
}
}
foreach (fibonacci(10) as $v) {
echo $v . "\n";
}
多个 yield
一个生成器函数中可以使用多个 yield 来生成值,当一个 yield 执行完成后,就会继续往下执行,直到所有的 yield 执行完毕。
<?php
function gen() {
yield 1;
yield 2;
yield 3;
}
foreach (gen() as $v) {
echo $v . "\n";
}
以上程序输出:
1
2
3
Generator::send
Generator::send 可以向生成器中传入一个值,再使用 yield 接收。
<?php
function printer() {
echo "I'm printer!".PHP_EOL;
while (true) {
$string = yield;
echo $string.PHP_EOL;
}
}
$printer = printer();
$printer->send('Hello world!');
$printer->send('Bye world!');
以上程序会输出:
I'm printer!
Hello world!
Bye world!
也可以把有这种比较头晕的写法 $data = (yield "hello"),这样既可以完成值的返回,也可以对外接受 send 传过来的值,我们来看一个简单的示例:
<?php
function gen() {
$data = (yield "hello");
var_dump($data, __LINE__);
}
$g = gen();
var_dump($g->current(), __LINE__);
var_dump($g->send("world"), __LINE__);
以上程序会输出:
string(5) "hello"
int(8)
string(5) "world"
int(4)
NULL
int(9)
以上程序先是 current
方法获取了 yield "hello"
返回的值,打印出 hello。接着程序往下走,使用 send("world")
向函数体内传送数据被 $data = (yield "hello")
接收到,程序往下走,则打印出 $data 的值:world。此时,函数体内没有剩余的 yield 了,则像普通函数一样返回一个 NULL,则 send("world")
的值为 NULL。
本文目录