ZetCode

PHP 生成器

最后修改于 2025 年 5 月 21 日

在本文中,我们探讨了 PHP 生成器,这是一个强大的功能,允许对大型数据集进行高效迭代,同时最大限度地减少内存使用。

生成器提供了一种轻量级的替代方案,用于替代传统的迭代器,它们一次只产生一个值,而不是将整个集合存储在内存中。 在 PHP 5.5 中引入,它们允许开发人员在大型数据源上使用 foreach 循环,而无需构建完整的数组,这使得它们非常适合有效地处理大型文件、数据库记录和 API 结果。

PHP 生成器的优点是

通过使用生成器,开发人员可以在处理大型数据集时优化性能和内存使用,这使它们成为 PHP 中高效数据处理的必备工具。

基本生成器语法

生成器是使用 yield 关键字而不是 return 的函数。 当调用时,它们返回一个可以被迭代的 Generator 对象。 每个 yield 产生一个值到迭代中。

basic_generator.php
<?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

内存效率

生成器是内存效率的,因为它们一次只生成一个值,这与必须将所有值存储在内存中的数组不同。 这使它们成为处理大型数据集的理想选择。

memory_efficiency.php
<?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 语法来完成的。

keyed_yields.php
<?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 的生成器委托,它允许一个生成器产生来自另一个生成器、arrayTraversable 对象的值。 这使得生成器组合和更简洁的代码成为可能。

generator_delegation.php
<?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 对象提供了几种方法,用于更高级地控制迭代。 其中包括用于与生成器进行双向通信的 sendthrowgetReturn

generator_methods.php
<?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

实际用例

生成器在许多实际场景中都很有用,在这些场景中,内存效率或惰性求值很重要。 以下是一些常见的实际示例。

real_world.php
<?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 生成器提供了一个强大的工具,用于高效迭代和处理大型数据集。 需要记住的关键点

生成器是一个多功能的功能,在处理数据序列时,可以显着提高内存使用率和代码清晰度。 它们在数据处理管道以及处理大型或无限序列时特别有用。

作者

我叫 Jan Bodnar,是一位充满激情的程序员,拥有丰富的编程经验。 自 2007 年以来,我一直在撰写编程文章。 迄今为止,我撰写了 1,400 多篇文章和 8 本电子书。 我拥有超过十年的编程教学经验。

列出 所有 PHP 教程