生成器

PHP 官网手册解释

生成器提供了一种更容易的方法来实现简单的对象迭代,相比较定义类实现 Iterator 接口的方式,性能开销和复杂性大大降低。

生成器允许你在 foreach 代码块中写代码来迭代一组数据而不需要在内存中创建一个数组, 那会使你的内存达到上限,或者会占据可观的处理时间。相反,你可以写一个生成器函数,就像一个普通的自定义函数一样, 和普通函数只返回一次不同的是, 生成器可以根据需要 yield 多次,以便生成需要迭代的值。

一个简单的例子就是使用生成器来重新实现 range() 函数。 标准的 range() 函数需要在内存中生成一个数组包含每一个在它范围内的值,然后返回该数组, 结果就是会产生多个很大的数组。 比如,调用 range(0, 1000000) 将导致内存占用超过 100 MB。

做为一种替代方法, 我们可以实现一个 xrange() 生成器, 只需要足够的内存来创建 Iterator 对象并在内部跟踪生成器的当前状态,这样只需要不到1K字节的内存。

个人理解

生成器是一个用来实现迭代功能的对象,因为数组在 PHP 编程中特特殊性,我们不必再事先准备好数组来帮助我们完成相关的迭代任务,减少内存开销,提升代码的性能和易用可读性。

自定义生成器函数
/**
 * @param int $start
 * @param int $end
 * @param int $step
 * @return Generator
 */
function xrange(int $start, int $end, int $step = 1)
{
    if ($start >= $end) {
        throw new LogicException("Logic error: parameter start must be less than parameter end!");
    }
    if ($start < 0) {
        throw new LogicException("Logic error: parameter start must be greater than zero!");
    }
    if ($step <= 0) {
        throw new LogicException("Logic error: parameter step must be greater than zero!");
    }
    for ($i = $start; $i <= $end; $i += $step) {
        yield $i;
    }
}
PHP 内置 range 函数
/**
 * Create an array containing a range of elements
 * @link https://php.net/manual/en/function.range.php
 * @param mixed $start <p>
 * First value of the sequence.
 * </p>
 * @param mixed $end <p>
 * The sequence is ended upon reaching the end value.
 * </p>
 * @param int|float $step [optional] <p>
 * If a step value is given, it will be used as the
 * increment between elements in the sequence. step
 * should be given as a positive number. If not specified,
 * step will default to 1.
 * </p>
 * @return array an array of elements from start to
 * end, inclusive.
 * @since 4.0
 * @since 5.0
 */
function range ($start, $end, $step = 1) {}
与 PHP 内置 range 函数内存对比
Artisan::command('test', function () {
    // 生成器
    $start_memory_usage = memory_get_usage();
    $middle = xrange(0, 1000000, 1);
    $end_memory_usage = memory_get_usage();
    $memory_usage = sprintf("%.3fKiB", ($end_memory_usage - $start_memory_usage) / 1024);
    dump($memory_usage);

    unset($middle);

    // 数组
    $start_memory_usage = memory_get_usage();
    $middle = range(0, 1000000, 1);
    $end_memory_usage = memory_get_usage();
    $memory_usage = sprintf("%.3fKiB", ($end_memory_usage - $start_memory_usage) / 1024);
    dump($memory_usage);
});

内存开销结果对比

与 PHP 内置 range 函数性能对比
Artisan::command('test', function () {
    // 生成器
    $start_microtime = microtime(true);
    foreach (xrange(0, 1000000, 1) as $value) {
    }
    $end_microtime = microtime(true);
    $microtime_usage = sprintf("%.5fms", ($end_microtime - $start_microtime) * 1000);
    dump($microtime_usage);

    // 数组
    $start_microtime = microtime(true);
    foreach (range(0, 1000000, 1) as $value) {
    }
    $end_microtime = microtime(true);
    $microtime_usage = sprintf("%.5fms", ($end_microtime - $start_microtime) * 1000);
    dump($microtime_usage);
});

与 PHP 内置 range 函数性能对比

生成器其他语法

自定义生成键值对

/**
 * @return Generator
 */
function range_test()
{
    $key = null;
    for ($i = 0; $i <= 7; $i++) {
        $key = sprintf('0X%08X', pow($i + 1, $i + 1));
        yield $key => $i;
    }
}


Artisan::command('test', function () {
    foreach (range_test() as $key => $value) {
        dump($key . ' => ' . $value);
    }
});

自定义键值对

yield 之引用
/**
 * @param $value
 * @return Generator
 */
function &range_test($value)
{
    for ($i = 0; $i < 44; $i++) {
        if ($value > 0) {
            yield $value;
        } else {
            return;
        }
    }
}

Artisan::command('test', function () {
    $value = 7;
    foreach (range_test($value) as &$item) {
        dump($item);
        $item--;
    }
});

yield 之引用

yield from 关键字之嵌套数组
/**
 * @return Generator
 */
function range_test()
{
    for ($i = 0; $i < 8; $i += 2) {
        yield from [$i, $i + 1];
    }
}


Artisan::command('test', function () {
    foreach (range_test() as $value) {
        dump($value);
    }
});

嵌套数组

yield from 关键字之嵌套生成器
/**
 * @return Generator
 */
function range_zero_to_two()
{
    for ($i = 0; $i <= 2; $i++) {
        yield $i;
    }
}


/**
 * @return Generator
 */
function range_function()
{
    for ($i = 1; $i <= 2; $i++) {
        yield from range_zero_to_two();
    }
}


Artisan::command('test', function () {
    foreach (range_function() as $value) {
        dump($value);
    }
});

嵌套运行结果

官网介绍

总结

在很多场景下,使用生成器来代替数组是具备很多优势的,比如临时需要一个超大的数组,就可以用生成器,yield 关键字会返回当前的值,暂停函数的执行,需要值的时候我们再调用,而不必事先准备好,有的大型数组可能会占用超过几十 MiB 的内存,而用生成器只需要不到 1KiB 的内存。性能上数组占据优势,通过测试,数组大约比生成器快 2.5 倍,但是数组的内存开销对比生成器却显得更为过分。生成器还具备数组不具备的其他优势,多重嵌套生成器、数组、迭代器等等。
当然,这些都需要针对实际的应用场景进行考量进而选择更优的方案。

迭代器

最后修改:2020 年 04 月 15 日 09 : 11 AM
如果觉得我的文章对你有用,请随意赞赏