PHP Conference Japan 2024

生成器总览

(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
11 years ago
for the protection from the leaking of resources
see RFC https://wiki.php.net/rfc/generators#closing_a_generator

and use finnaly

sample code

function 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
8 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
8 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
10 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
10 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