协程二:迭代生成器

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

yield的理解:yield关键字暂停当前方法,且返回值给上一级处理。当上一级处理完毕继续遍历时,继续冲上次暂停的地方继续执行。

以改写range函数为例。使用yield关键字创建生成器接口来迭代对象,比使用range保存数组再遍历,内存对比:

生成器:

<?php
/**
 * 生成器
 *
 * @author tangbo<admin@tbphp.net>
 */

namespace App;

use Generator;

class RangeGenerator
{
    private $len = 0;

    public function __construct(int $len)
    {
        $this->len = $len;
    }

    public function range(): Generator
    {
        for ($i = 0; $i <= $this->len; $i++) {
            yield $i;
        }
    }
}

测试:

<?php
use App\RangeGenerator;
use App\RangeIterator;
use PHPUnit\Framework\TestCase;

class RangeGeneratorTest extends TestCase
{

    private $size = 50000;

    /**
     * 测试原始range方法
     */
    public function testOldrange(): void
    {
        $this->result(memory_get_usage(), range(0, $this->size));
    }

    /**
     * 测试yield关键字生成器方法
     */
    public function testRange(): void
    {
        $this->result(memory_get_usage(), (new RangeGenerator($this->size))->range());
    }

    private function result($start, $range): void
    {
        foreach ($range as $v) {
            echo $v;
        }
        echo PHP_EOL;
        echo '内存:' . (memory_get_usage() - $start) . PHP_EOL;
    }
}

经过对比得知,生成器方式来迭代内存小很多,特别是长度越长越明显。原因是使用range函数是在遍历之前,就先把目标长度的值保存为一个数组,数组的大小和目标长度成正比。但是生成器是在遍历时,再生成需要的数值。

我们可以使用迭代器来自己实现这一过程:

<?php
/**
 * Description
 *
 * @author tangbo<admin@tbphp.net>
 */

namespace App;

use Iterator;

class RangeIterator implements Iterator
{

    private $size;
    private $key;

    public function __construct($size)
    {
        $this->size = $size;
    }

    public function current()
    {
        return $this->key;
    }

    public function next()
    {
        $this->key++;
    }

    public function key()
    {
        return $this->key;
    }

    public function valid()
    {
        return $this->key <= $this->size;
    }

    public function rewind()
    {
        $this->key = 0;
    }
}

在之前的测试类里面加入新的测试代码:

/**
 * 测试迭代器
 */
public function testIrange(): void
{
    $this->result(memory_get_usage(), new RangeIterator($this->size));
}

对比该结果,使用迭代器跟生成器内存差不多。不过使用yield关键字的生成器使用更简单,性能也更高,推荐使用。

return

使用yield同时也可以使用return返回值。

返回值需要调用getReturn方法获取,并且要在所有yield遍完之后才能获取返回值,否则会报错。

Last updated