浅谈 PHP 生成器

前言

本来是打算写一篇关于PHP协程调度的文章的,结果写的时候发现里面太多点都是需要介绍生成器,所以还是先单独写一篇介绍一下PHP生成器的相关知识作为前作吧。

生成器

首先让我们看下PHP官方文档中是怎么介绍的吧:

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

看了这句话,我们可以获得几个关键词: 对象迭代、 Iterator 接口、性能开销 ,比较抽象,talk is cheap show me the code,下面我们从一个生成器最经典的例子开始吧。

PHP中的range() 函数在使用的时候会在内存中创建一个包含指定范围单元的数组并返回,一般来说,这个并没什么不妥,但是当所传的limit入参值很大的时候,那么也就意味着将会在内存中创建的数组也会很大,这个就太恐怖了,这是要干爆内存的节奏啊。此时我们可以通过生成器来实现一个更高效的range函数,(下面的代码是把PHP官方文档中精简处理了一下):

function xrange($start, $limit, $step = 1)
{
    //校验参数,此处省略

    for ($i = $start; $i <= $limit; $i += $step) {
        //向外产出值
        yield $i;
    }
}

//xrange此时返回的是一个生成器对象
$gen = xrange(1, 9);

//对生成器进行迭代
foreach ($gen as $number) {
    echo "$number ";
}

这里在xrange和range函数的效果相同,均是产生了一个可迭代的变量,但是不同的是,range函数有点像ORM里面常说的 预加载 ,而xrange则是 懒加载 只是等到迭代到那个点才会产生对应的值,因此xrange并不需要分配大块内存来存放变量,大大节约了内存,提升效率。

现在我们来总结下生成器和普通函数有哪些异同:

  1. 生成器中必须包含yield关键字(用来生成结果),而且可以是多次出现,普通函数中向外部返回结果只能使用return,且函数执行完毕;
  2. 一个生成器不可以通过return返回值,这样做会产生一个编译错误PHP Fatal error: Generators cannot return values using "return"注意:这个在PHP7下面不会出错,但是会终止生成器继续执行,即调valid()方法会返回false,然而在PHP5中return空是一个有效的语法并且它将会终止生成器继续执行)

生成器类(Generator)

Generator 对象是从生成器返回的,上面代码中$gen就是一个生成器对象。注意,生成器对象和其他类的对象不同,它并不能通过new关键字创建,只能从生成器函数获取。首先我们看下Generator类摘要来看看其组成:

Generator implements Iterator
{
    /**
     * 返回当前产生的值(yield后面表达式的值)
     */
    public mixed current ( void )

    /**
     * yield的键(yield 'key'=>'val';)
     */
    public mixed key ( void )

    /**
     * 从上一个yield之后继续执行,直到下一个yield
     */
    public void next ( void )

    /**
     * 重置迭代器(对于生成器并没什么卵用)
     */
    public void rewind ( void )

    /**
     * 向生成器中传入一个值,并从上一个yield之后继续执行
     */
    public mixed send ( mixed $value )

    /**
     * 向生成器中抛出一个异常,并从上一个yield之后继续执行
     */
    public void throw ( Exception $exception )

    /**
     * 检查迭代器是否被关闭(false表示已关闭)
     */
    public bool valid ( void )

    /**
     * 序列化回调,但是生成器不能被序列化,因此会抛出一个异常
     */
    public void __wakeup ( void )
}

从上面的类摘要可以看出,Generator类是实现Iterator接口的,因此它具有迭代器的的特性。另外它加入了send()throw()__wekeup()方法,相关方法说明已经写了注释,在此不再赘述。

写了一堆,发现我的文笔不好,还是画个图感受一下吧(图画的也不美观,大家凑合着看吧,2333)
file

yield关键词

接下来让我们看下yield关键词,它最简单的调用形式看起来像普通函数的return,不同之处在于普通return会返回值并终止函数的执行,而yield会返回一个值给循环调用此生成器的代码,并且只是暂停执行生成器函数。

这是一个典型的yield表达式:$data = yield $key => $value;,该表达式包括两部分:

注意:PHP5需要加上括号$data = (yield $key => $value);,否则会产生一个编译错误,PHP7就不用关心这个了

  • 其一,是yield后面的表达式,这个可以是单个值也可以是键值对,与array中的一个元素对应,这部分表达式是返回给上层调用的,也就是上层可以通过current方法接收到值或者在执行send方法的返回值;
  • 另外一块就是yield关键词本身,个人把他理解成一个接收器,会收到send方法传入的值,这个值就是整个yield表达式当前的值,可以被左边的变量接收。

这么说可能有点抽象,还是上图吧:
file

生成器委托(yield from)

PHP7新增了yield from关键词,该语法开始允许从其他的generator,Traversable对象,或者数组通过yield from 生成数函数 来yield值。yield from的各种特性与yield一样都是生成数据,只是后面跟随的表达式不同。下面看个例子(摘自PHP官方文档):

function count_to_ten()
{
    yield 1;
    yield 2;
    yield from [3, 4];  //生成数组
    yield from new ArrayIterator([5, 6]);   //生成可遍历对象
    yield from seven_eight();   //生成生成器对象
    yield 9;
    yield 10;
}

function seven_eight()
{
    yield 7;
    yield from eight();
}

function eight()
{
    yield 8;
}

foreach (count_to_ten() as $num) {
    echo "$num ";
}

//输出:1 2 3 4 5 6 7 8 9 10 

yield from以方便我们编写比较清晰生成器嵌套,这点可以类比于函数中的嵌套调用,当函数A中调用另一个函数B,此时会等B执行完成并返回,方才继续执行。在没有yield from的时候,实现生成器嵌套需要自己实现栈并进行压栈和弹出操作以达到相同效果,那是多么痛苦的操作。

结束

好了,这就是我自己看生成器官方文档的一些收获总结,里面代码均是改编自文档,大家也可以自己直接研究文档。同时大家如果发现有什么错误,也请批评指出。

文中参考文档:

生成器
生成器类

本作品采用《CC 协议》,转载必须注明作者和本文链接
只要全力以赴就无所谓失败
本帖由系统于 6年前 自动加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 4

期待后续协程的文章

6年前 评论

@liangh 这段时间比较忙,等我放假回家弄吧

6年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!