生成器总览

(PHP 5 >= 5.5.0, PHP 7, PHP 8)

生成器提供了一种更容易的方法来实现简单的对象迭代,相比较定义类实现 Iterator 接口的方式,性能开销和复杂性大大降低。

生成器提供了方便的方式来向 foreach 循环提供数据,而无需提前在内存中构建数组,这可能会导致程序超出内存限制或需要相当长的处理时间来生成。相反,可以使用生成器函数,与普通的 function 相同,不同之处在于生成器不是 return 一次,而是可以根据需要多次 yield 以提供要迭代的值。与迭代器一样,无法进行随机数据访问。

一个简单的例子就是使用生成器来重新实现 range() 函数。 标准的 range() 函数需要在内存中生成一个数组包含每一个在它范围内的值,然后返回该数组, 结果就是会产生多个很大的数组。 比如,调用 range(0, 1000000) 将导致内存占用超过 100 MB。

做为一种替代方法, 我们可以实现一个 xrange() 生成器, 只需要足够的内存来创建 Iterator 对象并在内部跟踪生成器的当前状态,这样只需要不到1K字节的内存。

示例 #1 将 range() 实现为生成器

<?php
function xrange($start, $limit, $step = 1) {
if (
$start <= $limit) {
if (
$step <= 0) {
throw new
LogicException('Step must be positive');
}

for (
$i = $start; $i <= $limit; $i += $step) {
yield
$i;
}
} else {
if (
$step >= 0) {
throw new
LogicException('Step must be negative');
}

for (
$i = $start; $i >= $limit; $i += $step) {
yield
$i;
}
}
}

/*
* 注意下面range()和xrange()输出的结果是一样的。
*/

echo 'Single digit odd numbers from range(): ';
foreach (
range(1, 9, 2) as $number) {
echo
"$number ";
}
echo
"\n";

echo
'Single digit odd numbers from xrange(): ';
foreach (
xrange(1, 9, 2) as $number) {
echo
"$number ";
}
?>

以上示例会输出:

Single digit odd numbers from range():  1 3 5 7 9
Single digit odd numbers from xrange(): 1 3 5 7 9

Generator 对象

调用生成器函数时会返回一个内部的 Generator 类的对象。 该对象实现了 Iterator 接口,基本上和仅向前的迭代器一样, 它提供的方法可以操控生成器的状态,包括发送值、返回值。

添加备注

用户贡献的备注 6 notes

up
178
bloodjazman at gmail dot com
12 years ago
for the protection from the leaking of resources see RFC https://wiki.php.net/rfc/generators#closing_a_generatorand use finnalysample codefunction getLines($file) {    $f = fopen($file, 'r');    try {        while ($line = fgets($f)) {            yield $line;        }    } finally {        fclose($f);    }}foreach (getLines("file.txt") as $n => $line) {    if ($n > 5) break;    echo $line;}
up
47
montoriusz at gmail dot com
9 years ago
Bear in mind that execution of a generator function is postponed until iteration over its result (the Generator object) begins. This might confuse one if the result of a generator is assigned to a variable instead of immediate iteration.<?php$some_state = 'initial';function gen() {    global $some_state;     echo "gen() execution start\n";    $some_state = "changed";    yield 1;    yield 2;}function peek_state() {    global $some_state;    echo "\$some_state = $some_state\n";}echo "calling gen()...\n";$result = gen();echo "gen() was called\n";peek_state();echo "iterating...\n";foreach ($result as $val) {    echo "iteration: $val\n";    peek_state();}?>If you need to perform some action when the function is called and before the result is used, you'll have to wrap your generator in another function.<?php/**  * @return Generator  */function some_generator() {    global $some_state;    $some_state = "changed";    return gen();}?>
up
18
chung1905 at gmail dot com
5 years ago
In addition to the note of "montoriusz at gmail dot com": https://www.php.net/manual/en/language.generators.overview.php#119275"If you need to perform some action when the function is called and before the result is used, you'll have to wrap your generator in another function."You can use Generator::rewind instead (https://www.php.net/manual/en/generator.rewind.php)Sample code:<?php/** function/generator definition **/echo "calling gen()...\n";$result = gen();$result->rewind();echo "gen() was called\n";/** iteration **/?>
up
28
info at boukeversteegh dot nl
9 years ago
Here's how to detect loop breaks, and how to handle or cleanup after an interruption.<?php    function generator()    {        $complete = false;        try {            while (($result = some_function())) {                yield $result;            }            $complete = true;        } finally {            if (!$complete) {                // cleanup when loop breaks             } else {                // cleanup when loop completes            }        }        // Do something only after loop completes    }?>
up
19
lubaev
11 years ago
Abstract test.<?php$start_time=microtime(true);$array = array();$result = '';for($count=1000000; $count--;){  $array[]=$count/2;}foreach($array as $val){  $val += 145.56;  $result .= $val;}$end_time=microtime(true);echo "time: ", bcsub($end_time, $start_time, 4), "\n";echo "memory (byte): ", memory_get_peak_usage(true), "\n";?><?php$start_time=microtime(true);$result = '';function it(){  for($count=1000000; $count--;)  {    yield $count/2;  }}foreach(it() as $val){  $val += 145.56;  $result .= $val;}$end_time=microtime(true);echo "time: ", bcsub($end_time, $start_time, 4), "\n";echo "memory (byte): ", memory_get_peak_usage(true), "\n";?>Result:----------------------------------           |  time  | memory, mb |----------------------------------| not gen  | 2.1216 | 89.25      ||---------------------------------| with gen | 6.1963 | 8.75       ||---------------------------------| diff     | < 192% | > 90%      |----------------------------------
up
14
dc at libertyskull dot com
11 years ago
Same example, different results:----------------------------------           |  time  | memory, mb |----------------------------------| not gen  | 0.7589 | 146.75     ||---------------------------------| with gen | 0.7469 | 8.75       ||---------------------------------Time in results varying from 6.5 to 7.8 on both examples.So no real drawbacks concerning processing speed.
To Top