Skip to content

生成器返回值

概述

从 PHP 7.0 开始,生成器函数支持使用 return 语句返回一个最终值。这个值可以通过 Generator::getReturn() 方法在生成器执行完毕后获取。生成器的返回值与 yield 产出的值是不同的概念:yield 产出的值是迭代过程中的数据,而 return 的值是生成器完成后的最终结果。

PHP 版本

生成器中的 return 语句和 Generator::getReturn() 方法从 PHP 7.0 开始可用。

基础概念

return 在生成器中的行为

在生成器函数中,return 语句会终止生成器的执行,并设置一个最终返回值。这个值不会被 foreach 遍历到,需要通过 Generator::getReturn() 显式获取。

Generator::getReturn() 方法

getReturn() 在生成器执行完毕(状态为 Finished)后调用,返回 return 语句的值。如果在生成器尚未完成时调用,会抛出异常。

最终值的用途

生成器的最终返回值通常用于:

  • 返回聚合计算的结果(总和、平均值等)
  • 返回处理状态(成功/失败/统计信息)
  • 返回元数据或摘要

语法与代码

基本返回值

php
<?php
declare(strict_types=1);

function sumGenerator(array $numbers): \Generator
{
    $sum = 0;
    foreach ($numbers as $number) {
        $sum += $number;
        yield $number;
    }
    return $sum;
}

$gen = sumGenerator([1, 2, 3, 4, 5]);

// foreach 遍历产出的值
foreach ($gen as $value) {
    echo "产出: {$value}\n";
}
// 产出: 1
// 产出: 2
// 产出: 3
// 产出: 4
// 产出: 5

// 获取最终返回值
echo "总和: " . $gen->getReturn() . "\n"; // 总和: 15

getReturn 的调用时机

php
<?php
declare(strict_types=1);

function processGenerator(): \Generator
{
    yield 'step 1';
    yield 'step 2';
    return 'completed';
}

$gen = processGenerator();
echo $gen->current() . "\n"; // step 1

// getReturn 在生成器未完成时调用会抛出异常
try {
    $gen->getReturn();
} catch (\Exception $e) {
    echo "异常: " . $e->getMessage() . "\n";
    // Generator is still running (not finished)
}

$gen->next(); // step 2
$gen->next(); // 生成器结束
echo $gen->getReturn() . "\n"; // completed

调用时机

Generator::getReturn() 只能在生成器完全执行完毕后调用。在生成器仍处于 Suspended 状态时调用会抛出 Exception

返回复合值

php
<?php
declare(strict_types=1);

function analyzeGenerator(iterable $data): \Generator
{
    $count = 0;
    $sum = 0;
    $min = PHP_INT_MAX;
    $max = PHP_INT_MIN;

    foreach ($data as $value) {
        $count++;
        $sum += $value;
        $min = min($min, $value);
        $max = max($max, $value);
        yield $value;
    }

    return [
        'count' => $count,
        'sum' => $sum,
        'average' => $count > 0 ? $sum / $count : 0,
        'min' => $min === PHP_INT_MAX ? null : $min,
        'max' => $max === PHP_INT_MIN ? null : $max,
    ];
}

$gen = analyzeGenerator([15, 23, 8, 42, 31, 19]);

foreach ($gen as $value) {
    // 处理每个值
}

$stats = $gen->getReturn();
print_r($stats);
// Array ( [count] => 6 [sum] => 138 [average] => 23 [min] => 8 [max] => 42 )

不使用 return 的生成器

php
<?php
declare(strict_types=1);

function noReturnGenerator(): \Generator
{
    yield 1;
    yield 2;
    // 没有 return 语句
}

$gen = noReturnGenerator();
foreach ($gen as $v) {
    // 遍历产出值
}
echo $gen->getReturn(); // 输出: (空)— 返回 null

详细说明

return vs yield 的区别

特性yieldreturn
执行效果暂停生成器终止生成器
获取方式current() / foreachgetReturn()
多次使用可以多次 yield只能 return 一次
返回类型任意值任意值
foreach 可见

yield from 与 return 值

当使用 yield from 委托子生成器时,子生成器的 return 值会被 yield from 表达式捕获。

php
<?php
declare(strict_types=1);

function subTask(): \Generator
{
    yield 'a';
    yield 'b';
    return 'sub result';
}

function mainTask(): \Generator
{
    yield 'start';

    // yield from 表达式的值就是子生成器的 return 值
    $subResult = yield from subTask();

    yield "sub returned: {$subResult}";

    return 'main result';
}

$gen = mainTask();

foreach ($gen as $value) {
    echo "{$value}\n";
}
// start
// a
// b
// sub returned: sub result

echo $gen->getReturn(); // main result

getReturn 与迭代方式的关系

无论使用 foreach 还是手动调用 current()/next(),只要生成器完全执行完毕,getReturn() 都能正常工作。

php
<?php
declare(strict_types=1);

function dualAccess(): \Generator
{
    yield 'first';
    yield 'second';
    return 'final';
}

// 方式一:foreach
$gen1 = dualAccess();
foreach ($gen1 as $v) { /* ... */ }
echo $gen1->getReturn(); // final

// 方式二:手动迭代
$gen2 = dualAccess();
while ($gen2->valid()) {
    $gen2->next();
}
echo $gen2->getReturn(); // final

实战示例

实战:数据处理管道的统计信息

php
<?php
declare(strict_types=1);

function dataProcessor(iterable $source): \Generator
{
    $processed = 0;
    $skipped = 0;
    $errors = 0;

    foreach ($source as $item) {
        try {
            $result = processItem($item);
            if ($result !== null) {
                yield $result;
                $processed++;
            } else {
                $skipped++;
            }
        } catch (\Throwable $e) {
            $errors++;
        }
    }

    return [
        'processed' => $processed,
        'skipped' => $skipped,
        'errors' => $errors,
    ];
}

function processItem(mixed $item): ?string
{
    if (!is_numeric($item)) {
        return null;
    }
    return 'item_' . ((int) $item * 2);
}

$source = [1, 'invalid', 3, null, 5, 'text', 7];
$gen = dataProcessor($source);

foreach ($gen as $result) {
    echo "处理结果: {$result}\n";
}

$report = $gen->getReturn();
echo "处理: {$report['processed']}, 跳过: {$report['skipped']}, 错误: {$report['errors']}\n";
// 处理: 4, 跳过: 1, 错误: 2

实战:文件处理的统计

php
<?php
declare(strict_types=1);

function countLinesAndWords(string $filePath): \Generator
{
    $lineCount = 0;
    $wordCount = 0;
    $byteCount = 0;

    $handle = fopen($filePath, 'r');
    if ($handle === false) {
        throw new \RuntimeException("无法打开文件");
    }

    try {
        while (($line = fgets($handle)) !== false) {
            $lineCount++;
            $wordCount += str_word_count($line);
            $byteCount += strlen($line);
            yield $line;
        }
    } finally {
        fclose($handle);
    }

    return [
        'lines' => $lineCount,
        'words' => $wordCount,
        'bytes' => $byteCount,
    ];
}

// 使用生成器逐行处理文件,同时获取统计信息
$gen = countLinesAndWords('example.txt');

foreach ($gen as $line) {
    if (str_contains($line, 'TODO')) {
        echo "发现 TODO: " . trim($line) . "\n";
    }
}

$stats = $gen->getReturn();
echo "行数: {$stats['lines']}, 词数: {$stats['words']}, 字节数: {$stats['bytes']}\n";

实战:聚合生成器

php
<?php
declare(strict_types=1);

function aggregateFromMultiple(iterable ...$sources): \Generator
{
    $totalProcessed = 0;

    foreach ($sources as $source) {
        foreach ($source as $item) {
            yield $item;
            $totalProcessed++;
        }
    }

    return $totalProcessed;
}

$data1 = [1, 2, 3];
$data2 = ['a', 'b'];
$data3 = [true, false];

$gen = aggregateFromMultiple($data1, $data2, $data3);
foreach ($gen as $item) {
    // 处理每一项
}
echo "总计处理: " . $gen->getReturn() . " 项\n";
// 总计处理: 7 项

注意事项

PHP 7.0 之前不支持 return

在 PHP 5.x 中,生成器函数中的 return 只能用于终止生成器,不能带有值(return; 或省略 return 是可以的,但 return $value; 会导致语法错误)。

空生成器的 getReturn

php
<?php
declare(strict_types=1);

function emptyGenerator(): \Generator
{
    return 'empty result';
    // 没有 yield,生成器直接结束
}

$gen = emptyGenerator();
echo $gen->getReturn(); // empty result

// 如果既没有 yield 也没有 return
function trulyEmpty(): \Generator
{
    // 空
}
$gen2 = trulyEmpty();
echo $gen2->getReturn(); // (null)

return 值的类型

return 的值可以是任意类型,包括 null、标量类型、数组、对象等。在 PHP 8.1+ 中,你也可以使用枚举作为返回值。

php
<?php
declare(strict_types=1);

enum Status: string
{
    case Success = 'ok';
    case Partial = 'partial';
    case Failed = 'failed';
}

function processData(iterable $data): \Generator
{
    $hasError = false;

    foreach ($data as $item) {
        try {
            yield transform($item);
        } catch (\Throwable) {
            $hasError = true;
        }
    }

    return $hasError ? Status::Partial : Status::Success;
}

最佳实践

  1. 利用返回值传递聚合信息:将生成器的最终统计、状态等通过 return 返回,而不是修改外部变量。
  2. 在 finally 中确保返回值可用:即使生成器被中断,getReturn() 仍可获取 return 值。
  3. 文档化返回值类型:在 PHPDoc 中注明 return 值的类型和含义。
  4. 与 yield from 配合使用:利用 yield from 表达式捕获子生成器的返回值。
  5. 考虑使用专用结果对象:当返回值较复杂时,使用值对象(DTO)封装。
php
<?php
declare(strict_types=1);

readonly class ProcessingResult
{
    public function __construct(
        public int $processed,
        public int $skipped,
        public int $errors
    ) {}

    public function isSuccess(): bool
    {
        return $this->errors === 0;
    }
}

function processItems(iterable $items): \Generator
{
    $stats = ['processed' => 0, 'skipped' => 0, 'errors' => 0];

    foreach ($items as $item) {
        // 处理逻辑...
        yield $item;
        $stats['processed']++;
    }

    return new ProcessingResult(
        $stats['processed'],
        $stats['skipped'],
        $stats['errors']
    );
}

参考链接