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 教程。