PHP 生成器
最后修改于 2025 年 5 月 21 日
在本文中,我们探讨了 PHP 生成器,这是一个强大的功能,允许对大型数据集进行高效迭代,同时最大限度地减少内存使用。
生成器提供了一种轻量级的替代方案,用于替代传统的迭代器,它们一次只产生一个值,而不是将整个集合存储在内存中。 在 PHP 5.5 中引入,它们允许开发人员在大型数据源上使用 foreach 循环,而无需构建完整的数组,这使得它们非常适合有效地处理大型文件、数据库记录和 API 结果。
PHP 生成器的优点是
- 内存效率: 避免将整个数据集加载到内存中,从而提高性能。
- 惰性求值: 仅在需要时计算值,减少处理开销。
- 更简单的迭代器实现: 无需手动实现
Iterator接口。 - 流处理: 非常适合迭代大型文件或数据库查询。
- 提高性能: 与在大型数据集上使用传统循环相比,减少了执行时间。
通过使用生成器,开发人员可以在处理大型数据集时优化性能和内存使用,这使它们成为 PHP 中高效数据处理的必备工具。
基本生成器语法
生成器是使用 yield 关键字而不是 return 的函数。 当调用时,它们返回一个可以被迭代的 Generator 对象。 每个 yield 产生一个值到迭代中。
<?php
declare(strict_types=1);
function numberGenerator(int $start, int $end): Generator {
for ($i = $start; $i <= $end; $i++) {
yield $i;
}
}
// Using the generator
foreach (numberGenerator(1, 5) as $number) {
echo "$number\n";
}
// Generator returns a Generator object
$generator = numberGenerator(1, 3);
echo get_class($generator) . "\n"; // Generator
// Manual iteration
$generator = numberGenerator(10, 12);
echo $generator->current() . "\n"; // 10
$generator->next();
echo $generator->current() . "\n"; // 11
这展示了一个基本生成器,它产生一个范围内的数字。 生成器在 yield 之间保留其状态,允许它从中断的地方继续。 生成器是可中断的函数,可以暂停和恢复。
λ php basic_generator.php 1 2 3 4 5 Generator 10 11
内存效率
生成器是内存效率的,因为它们一次只生成一个值,这与必须将所有值存储在内存中的数组不同。 这使它们成为处理大型数据集的理想选择。
<?php
declare(strict_types=1);
// Memory intensive approach
function getLinesFromFile(string $filename): array {
$lines = [];
$file = fopen($filename, 'r');
while (!feof($file)) {
$lines[] = trim(fgets($file));
}
fclose($file);
return $lines;
}
// Generator approach
function getLinesFromFileGenerator(string $filename): Generator {
$file = fopen($filename, 'r');
while (!feof($file)) {
yield trim(fgets($file));
}
fclose($file);
}
// Memory usage comparison
$filename = 'large_file.txt';
// Array approach
$memoryBefore = memory_get_usage();
$lines = getLinesFromFile($filename);
$memoryAfter = memory_get_usage();
echo "Array memory: " . ($memoryAfter - $memoryBefore) . " bytes\n";
// Generator approach
$memoryBefore = memory_get_usage();
$linesGenerator = getLinesFromFileGenerator($filename);
$memoryAfter = memory_get_usage();
echo "Generator memory: " . ($memoryAfter - $memoryBefore) . " bytes\n";
// Process large file without loading it all
foreach (getLinesFromFileGenerator($filename) as $line) {
// Process each line without memory issues
if (str_contains($line, 'error')) {
echo "Found error in line: $line\n";
}
}
生成器版本使用更少的内存,因为它一次只在内存中保存一行。 当处理大到无法一次性全部装入内存的文件时,这种差异变得至关重要。
λ php memory_efficiency.php Array memory: 10485760 bytes Generator memory: 416 bytes Found error in line: Sample error line 1 Found error in line: Sample error line 2
键值 yield
生成器可以产生键值对,这使得它们适合生成关联序列。 这是通过产生一个数组或使用 yield key => value 语法来完成的。
<?php
declare(strict_types=1);
function assocGenerator(): Generator {
yield 'name' => 'Alice';
yield 'age' => 30;
yield 'occupation' => 'Developer';
}
foreach (assocGenerator() as $key => $value) {
echo "$key: $value\n";
}
// More complex example
function csvGenerator(string $filename): Generator {
$file = fopen($filename, 'r');
// Get headers
$headers = fgetcsv($file);
while ($row = fgetcsv($file)) {
yield array_combine($headers, $row);
}
fclose($file);
}
// Process CSV file
foreach (csvGenerator('data.csv') as $record) {
echo "Processing: {$record['name']} ({$record['email']})\n";
}
// Numeric keys
function indexedGenerator(): Generator {
for ($i = 0; $i < 5; $i++) {
yield $i => "Item $i";
}
}
foreach (indexedGenerator() as $index => $item) {
echo "$index: $item\n";
}
这演示了生成器如何生成关联数组和处理结构化数据(如 CSV 文件)。 键值对被直接产生,并且可以在 foreach 循环中使用,就像常规的关联数组一样。
λ php keyed_yields.php name: Alice age: 30 occupation: Developer Processing: John Doe (john@example.com) Processing: Jane Smith (jane@example.com) 0: Item 0 1: Item 1 2: Item 2 3: Item 3 4: Item 4
生成器委托
PHP 7.0 引入了使用 yield from 的生成器委托,它允许一个生成器产生来自另一个生成器、array 或 Traversable 对象的值。 这使得生成器组合和更简洁的代码成为可能。
<?php
declare(strict_types=1);
function countTo3(): Generator {
yield 1;
yield 2;
yield 3;
}
function countTo6(): Generator {
yield from countTo3();
yield 4;
yield 5;
yield 6;
}
foreach (countTo6() as $number) {
echo "$number ";
}
echo "\n";
// Yield from array
function yieldArray(): Generator {
yield from ['a', 'b', 'c'];
}
foreach (yieldArray() as $letter) {
echo "$letter ";
}
echo "\n";
// Complex delegation
function readMultipleFiles(array $filenames): Generator {
foreach ($filenames as $filename) {
yield from readFileLines($filename);
}
}
function readFileLines(string $filename): Generator {
$file = fopen($filename, 'r');
while (!feof($file)) {
yield trim(fgets($file));
}
fclose($file);
}
// Process multiple files as one sequence
foreach (readMultipleFiles(['file1.txt', 'file2.txt']) as $line) {
echo "Line: $line\n";
}
// Yield from with keys
function combinedGenerator(): Generator {
yield from ['a' => 1, 'b' => 2];
yield from ['c' => 3];
yield 'd' => 4;
}
foreach (combinedGenerator() as $k => $v) {
echo "$k:$v ";
}
echo "\n";
生成器委托通过允许它们无缝组合来简化处理多个生成器的工作。 yield from 语法在内部处理所有迭代,使代码更简洁、更模块化。
λ php generator_delegation.php 1 2 3 4 5 6 a b c Line: First line of file1 Line: Second line of file1 Line: First line of file2 a:1 b:2 c:3 d:4
生成器方法
Generator 对象提供了几种方法,用于更高级地控制迭代。 其中包括用于与生成器进行双向通信的 send、throw 和 getReturn。
<?php
declare(strict_types=1);
function interactiveGenerator(): Generator {
$value = yield 'first';
echo "Received: $value\n";
$value = yield 'second';
echo "Received: $value\n";
return 'done';
}
$gen = interactiveGenerator();
// Get first yielded value
echo "Yielded: " . $gen->current() . "\n";
// Send value to generator
$gen->send('hello');
// Get next yielded value
echo "Yielded: " . $gen->current() . "\n";
// Send another value
$gen->send('world');
// Get return value
try {
$gen->next();
echo "Returned: " . $gen->getReturn() . "\n";
} catch (Exception $e) {
echo "Exception: " . $e->getMessage() . "\n";
}
// Exception handling in generators
function failingGenerator(): Generator {
try {
yield 'start';
throw new Exception('Generator error');
} catch (Exception $e) {
yield 'caught: ' . $e->getMessage();
}
yield 'end';
}
$gen = failingGenerator();
echo $gen->current() . "\n";
$gen->next();
echo $gen->current() . "\n";
$gen->next();
echo $gen->current() . "\n";
这演示了与生成器的双向通信。 可以使用 send() 将值发送到生成器中,并可以使用 throw() 将异常抛入其中。 迭代完成后,可以通过 getReturn() 访问返回值。
λ php generator_methods.php Yielded: first Received: hello Yielded: second Received: world Returned: done start caught: Generator error end
实际用例
生成器在许多实际场景中都很有用,在这些场景中,内存效率或惰性求值很重要。 以下是一些常见的实际示例。
<?php
declare(strict_types=1);
// 1. Processing large files
function processLargeFile(string $filename): Generator {
$file = fopen($filename, 'r');
while (!feof($file)) {
$line = fgets($file);
yield json_decode($line, true);
}
fclose($file);
}
// 2. Paginating database results
function paginateResults(PDO $pdo, string $query, int $pageSize = 100): Generator {
$offset = 0;
do {
$stmt = $pdo->prepare($query . " LIMIT ? OFFSET ?");
$stmt->execute([$pageSize, $offset]);
$results = $stmt->fetchAll();
yield from $results;
$offset += $pageSize;
} while (count($results) === $pageSize);
}
// 3. Generating infinite sequences
function fibonacciSequence(): Generator {
$a = 0;
$b = 1;
while (true) {
yield $a;
[$a, $b] = [$b, $a + $b];
}
}
$fib = fibonacciSequence();
for ($i = 0; $i < 10; $i++) {
echo $fib->current() . " ";
$fib->next();
}
echo "\n";
// 4. Processing API responses
function fetchPaginatedAPI(string $baseUrl): Generator {
$url = $baseUrl;
do {
$response = json_decode(file_get_contents($url), true);
yield from $response['data'];
$url = $response['next_page'] ?? null;
} while ($url);
}
// 5. Data transformation pipeline
function transformData(iterable $source, callable ...$transformers): Generator {
foreach ($source as $item) {
$result = $item;
foreach ($transformers as $transformer) {
$result = $transformer($result);
}
yield $result;
}
}
// Example pipeline
$data = [' apple ', ' banana ', ' cherry '];
$pipeline = transformData(
$data,
fn($s) => trim($s),
fn($s) => strtoupper($s),
fn($s) => "FRUIT: $s"
);
foreach ($pipeline as $item) {
echo "$item\n";
}
这些示例展示了生成器优雅地解决了实际问题。 从处理大型数据集到创建处理管道,生成器提供了内存效率高的解决方案,而传统数组难以做到。
λ php real_world.php 0 1 1 2 3 5 8 13 21 34 FRUIT: APPLE FRUIT: BANANA FRUIT: CHERRY
最佳实践
在使用生成器时,请遵循这些最佳实践,以编写简洁、高效且可维护的代码。
对于小型数据集,性能考虑至关重要。 在许多情况下,数组比生成器提供更好的效率,因为它们允许直接索引和更快地迭代,而无需维护生成器状态的开销。
// Bad:
function getAllUsers(): array {
$users = [];
// Fetch all from DB
return $users;
}
// Good:
function getAllUsersGenerator(): Generator {
// Fetch one at a time
while ($user = fetchUserFromDB()) {
yield $user;
}
}
使用生成器处理大型数据集的内存效率
/** @return Generator<int, User, mixed, void> */
function getUserGenerator(): Generator {
// ...
}
记录生成器返回类型。
function combinedData(): Generator {
yield from getUsers();
yield from getProducts();
}
使用 yield from 进行生成器组合。
function readFileWithCleanup(string $filename): Generator {
$file = fopen($filename, 'r');
try {
while (!feof($file)) {
yield trim(fgets($file));
}
} finally {
fclose($file);
}
}
在生成器中清理资源。
$gen = someGenerator();
foreach ($gen as $value) {
// ...
}
// Can't iterate again - generator is exhausted
// foreach ($gen as $value) {} // Won't work
请注意,生成器是单向的。 一旦生成器耗尽,它就无法重置或重用。 如果您需要多次迭代相同的值,请考虑将它们存储在数组中或使用不同的生成器。
function filter(iterable $items, callable $predicate): Generator {
foreach ($items as $item) {
if ($predicate($item)) {
yield $item;
}
}
}
使用生成器进行惰性求值。
// Works well with match, arrow functions, etc.
function transformGenerator(iterable $items): Generator {
foreach ($items as $item) {
yield match(true) {
is_int($item) => $item * 2,
is_string($item) => strtoupper($item),
default => $item
};
}
}
将生成器与其他语言特性(如 match 和箭头函数)一起使用,以获得更简洁的代码。
/**
* @yield int The generated ID
* @yield string The associated name
*/
function idNameGenerator(): Generator {
// ...
}
记录预期的 yield 以获得更好的代码清晰度。
这些做法有助于确保有效地使用生成器。 关键点包括适当的文档记录、资源清理以及选择生成器以提高内存效率,而不是在所有情况下都为了方便性。
PHP 生成器提供了一个强大的工具,用于高效迭代和处理大型数据集。 需要记住的关键点
- 生成器使用
yield一次产生一个值。 - 内存效率高的替代方案,用于构建完整的数组。
- 在
yield操作之间保持状态。 - 可以产生键值对,用于关联迭代。
- 支持委托,使用
yield from(PHP 7.0+)。 - 通过
send和throw启用双向通信。 - 迭代完成后,返回值可用 (PHP 7.0+)。
- 单向迭代(一旦进行就无法倒回)。
- 非常适合大型数据集 和惰性求值技术。
生成器是一个多功能的功能,在处理数据序列时,可以显着提高内存使用率和代码清晰度。 它们在数据处理管道以及处理大型或无限序列时特别有用。
作者
列出 所有 PHP 教程。