PHP的生成器(generator)

什么是生成器

生成器其实就是一个用于迭代的迭代器。它提供了一种更容易的方式来实现对象迭代,相比定义类实现 Iterator 接口的方式大大降低性能开销和复杂度。

看以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function test1()
{
for ($i = 0; $i < 3; $i++) {
yield $i + 1;
}
yield 1000;
yield 1001;
}

foreach (test1() as $t) {
echo $t, PHP_EOL;
}

// 1
// 2
// 3
// 1000
// 1001

这段代码很简单,首先生成器必须在方法中并使用 yield 关键字;其次每一个 yield 都可以看作是一次 return ;最后外部循环时每次循环取一个 yield 的返回值。

在上面例子中,循环了 3 次并返回了 1、2、3 这三个数字。然后在循环外部又写了 2 行 yield 分别输出了 1000 和 1001。因此外部的 foreach 循环一共输出了 5 次。

明明是一个方法,为什么它能够循环,而且返回一种循环体的格式。我们直接打印这个 test() 方法看看打印的是什么:

1
2
3
4
5
// 是一个生成器对象
var_dump(test1());
// Generator Object
// (
// )

上面的例子告诉你:当使用了 yield 进行内容返回后,返回的是一个 Generator 对象。这个对象就叫作生成器对象,这个对象是不能直接被 new 实例化,只能通过生成器函数这种方式返回实例。

Generator 这个类包含 current() 、 key() 等方法,最主要的这个类实现了 Iterator 接口,也就是说它就是一个特殊的迭代器类。

1
2
3
4
5
6
7
8
9
10
11
Generator implements Iterator {
/* 方法 */
public current ( void ) : mixed
public key ( void ) : mixed
public next ( void ) : void
public rewind ( void ) : void
public send ( mixed $value ) : mixed
public throw ( Exception $exception ) : void
public valid ( void ) : bool
public __wakeup ( void ) : void
}

生成器有什么用

生成器最强大的部分就在于它不需要一个数组或者任何的数据结构来保存这一系列数据。每次迭代执行到 yield 时都是动态返回的。所以生成器能在很大程度上节约内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 内存占用测试
$start_time = microtime(true);
function test2($clear = false)
{
$arr = [];
if($clear){
$arr = null;
return;
}
for ($i = 0; $i < 1000000; $i++) {
$arr[] = $i + 1;
}
return $arr;
}
$array = test2();
foreach ($array as $val) {
}
$end_time = microtime(true);
echo "time: ", bcsub($end_time, $start_time, 4), PHP_EOL;
echo "memory (byte): ", memory_get_usage(true), PHP_EOL;
// time: 0.0513
// memory (byte): 35655680

$start_time = microtime(true);
function test3()
{
for ($i = 0; $i < 1000000; $i++) {
yield $i + 1;
}
}
$array = test3();
foreach ($array as $val) {
}
$end_time = microtime(true);
echo "time: ", bcsub($end_time, $start_time, 4), PHP_EOL;
echo "memory (byte): ", memory_get_usage(true), PHP_EOL;
// time: 0.0517
// memory (byte): 2097152

以上代码进行 1000000 个循环后获取结果的简单操作,你可以可以直观地看出使用生成器的版本仅仅消耗了大约 2M 的内存,而未使用生成器的却消耗了 35M 的内存,有 10 多倍的差距了,而且越大的数据量差距会更加明显。所以又有人说生成器是 PHP 中最不可被低估了的一个特性。


生成器的应用
生成器主要应用方式有哪些呢。下面我们来看看:
1 返回空值以及中断
生成器可以返回空值,也就是说不带任何值就可以返回一个空值了,你直接 yield 就好了;用 return 来中断生成器的继续执行。

下面的代码我们在 $i = 4; 的时候返回了空值,也就是不会输出 5 (因为我们返回的是 $i + 1 )。然后在 $i == 7 的时候使用 return;来中断生成器的继续执行,也就是说循环最多只会输出到 7 ,就结束了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 返回空值以及中断
function test4()
{
for ($i = 0; $i < 10; $i++) {
if ($i == 4) {
yield; // 返回null值
}
if ($i == 7) {
return; // 中断生成器执行
}
yield $i + 1;
}
}
foreach (test4() as $t) {
echo $t, PHP_EOL;
}
// 1
// 2
// 3
// 4
// 5
// 6
// 7

2 返回键值对形式
生成器也可以返回键值对形式的可遍历对象,可以供 foreach 使用的,语法:yield key => value; 和数组项的定义形式一样,非常直观。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function test5()
{
for ($i = 0; $i < 10; $i++) {
yield 'key.' . $i => $i + 1;
}
}
foreach (test5() as $k=>$t) {
echo $k . ':' . $t, PHP_EOL;
}
// key.0:1
// key.1:2
// key.2:3
// key.3:4
// key.4:5
// key.5:6
// key.6:7
// key.7:8
// key.8:9
// key.9:10

外部传递数据

我们也可以通过 Generator::send 方法来向生成器中传入一个值。这个值将会被当做生成器当前 yield 的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function test6()
{
for ($i = 0; $i < 10; $i++) {
// 正常获取循环值,当外部send过来值后,yield获取到的就是外部传来的值了
$data = (yield $i + 1);
if($data == 'stop'){
return;
}
}
}
$t6 = test6();
foreach($t6 as $t){
if($t == 3){
$t6->send('stop');
}
echo $t, PHP_EOL;
}
// 1
// 2
// 3

正常获取循环值,当外部 send 过来值后,yield 获取到的就是外部传来的值了。记住:变量获取 yield 的值时必须要用括号括起来。

yield from 语法
这种语法其实就是指从另一个可迭代对象中一个一个的获取数据并形成生成器返回。看代码比较好理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function test7()
{
yield from [1, 2, 3, 4];
yield from new ArrayIterator([5, 6]);
yield from test1();
}
foreach (test7() as $t) {
echo 'test7:', $t, PHP_EOL;
}
// test7:1
// test7:2
// test7:3
// test7:4
// test7:5
// test7:6
// test7:1
// test7:2
// test7:3
// test7:1000

在 以上方法中使用了 yield from 分别从普通数组、迭代器对象和另一个生成器中获取了数据,并做为当前生成器的内容进行返回。

生成器可以用 count 获取数量吗?

测试一下就知道了,生成器是不能用 count 来获取它的数量的。使用 count 获取生成器的数量将会直接 Warning 警告。直接输出将会一直显示是 1 ,因为 count 的特性(强制转换成数组都会显示 1 )。

1
2
$c = count(test1()); // Warning: count(): Parameter must be an array or an object that implements Countable
// echo $c, PHP_EOL;

总结
yield 生成器不仅大大的节约了内存的开销,而且语法其实也非常的简洁明了。我们不需要在方法内部再多定义一个数组去存储返回值,直接 yield 就可以了。在实际的项目中你可以多多的使用这种生成器哦!