PHP 从Iterator理解Generator和yield

南风码哥| 阅读:825 更新时间:2017-01-30 PHP常用
写这篇文章是因为学习到了yield关键字,写过很多PHP程序,但是从来没有使用过这个语法,所以一定要探索一下。不研究不知道,一研究吓一跳,从yield关键字牵扯出来的内容真的很多,Iterator、Generator、协程、协程与线程的区别、协程与异步,甚至还让我怀疑了自己从书本上学到的线程和进程。这篇文章就从Iterator说起,简单的介绍下yield语法,下篇再写yield与协程。

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

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

你可以先不用完全理解上面这段话,只需要知道Generator主要的作用,我们先来看看迭代器是如何运行的。

1.Iterator接口

如果我们要使用foreach循环遍历对象,就要实现Iterator接口,Iterator接口提供5个方法:

  1. Iterator extends Traversable {
    /* Methods */
    abstract public mixed current ( void ) //返回当前位置的元素
    abstract public scalar key ( void ) //返回当前元素对应的key
    abstract public void next ( void ) //移到指向下一个元素的位置
    abstract public void rewind ( void ) //倒回到指向第一个元素的位置
    abstract public boolean valid ( void ) //判断当前位置是否有效
    }

foreach在循环的时候会调用这些方法,如果你仔细想想就可以知道如何调用这些方法的,不过你可以执行下面的代码,看看过程:


  1. <?php
    class testIter implements Iterator
    {
    private $_items = array(1,2,3,4,5,6,7);
    public function rewind() {
    echo "重置了数组指针\n";
    reset($this->_items);
    }
    public function current() {
    echo "访问了当前value\n";
    return current($this->_items);
    }
    public function key() {
    echo "访问了当前key\n";
    return key($this->_items);
    }
    public function next() {
    echo "移动数组指针\n";
    return next($this->_items);
    }
    public function valid() {
    echo "验证当前是否到最后了\n";
    return ($this->current() !== false);
    }
    }
    $iter = new testIter();
    $i = 1;
    foreach ($iter as $key => $value) {
    echo "循环访问".$i."次\n";
    echo "key:".$key." value:".$value."\n";
    echo "——————————————————————————————————\n" 
    }2.Generator

在官方文档上是这么定义Generator的:


  1. Generator implements Iterator {
    /* Methods */
    public mixed current ( void )
    public mixed key ( void )
    public void next ( void )
    public void rewind ( void )
    public mixed send ( mixed $value )
    public mixed throw ( Exception $exception )
    public bool valid ( void )
    public void __wakeup ( void )
    }

它实现了Iterator接口,所以是可以被foreach使用的对象。

Generator对象只能通过生成器来返回,生成器就是使用了yield关键字的一个函数,这个函数将会返回一个Generator对象。它不能通过new来实例化,只有仅此一个方法。所以这里要记清楚使用了yield的函数就不是普通函数了,是一个生成器,将会返回一个Generator对象。

你可以试试如下代码:


  1. <?php
    function testYield() {
    yield "hello world";
    }
    var_dump(testYield());
    它将打印出:
    
    class Generator#1 (0) {
    }

 

3.yield

我们知道了使用yield语法的函数会返回Generator对象,Generator对象又可以被foreach调用,看个简单的例子:


  1. <?php
    function myRange($start, $end) {
    for ($i = $start; $i <= $end; $i++) {
    yield $i;
    }
    }
    foreach (myRange(1, 10) as $value) {
    var_dump($value);
    }

也许这里很难理解,yield语句有点像return语句,代码执行到yield语句,generator函数的执行就会终止,并且会返回yield语句中的表达式的值给Generator对象,这跟return语句一样,不同的是,这返回值只是作为遍历Generator对象的当前元素,而不能赋值给其他变量。

这句话的意和Iterator的感觉很像对吧,我们就来看看他的本质是什么,执行这段代码:


  1. <?php
    function testYield() {
    echo "马上要执行yield1\n";
    yield "yield返回的第1个数据\n";
    echo "yield1继续,马上要执行yield2\n";
    yield "yield返回的第2个数据\n";
    echo "yield2继续,马上要执行yield3\n";
    yield "yield返回的第3个数据\n";
    echo "yield3继续,没yield了\n";
    }
    $gen = testYield();
    var_dump($gen->valid());
    echo $gen->current();
    echo $gen->current();
    $gen->next();
    echo $gen->current();
    echo $gen->current();
    $gen->next();
    echo $gen->current();
    $gen->next();
    var_dump($gen->valid());

它将打印出以下内容:

  1. 马上要执行yield1
  2. testForeach.php:13:
  3. bool(true)
  4. yield返回的第1个数据
  5. yield返回的第1个数据
  6. yield1继续,马上要执行yield2
  7. yield返回的第2个数据
  8. yield返回的第2个数据
  9. yield2继续,马上要执行yield3
  10. yield返回的第3个数据
  11. yield3继续,没yield了
  12. testForeach.php:22:
  13. bool(false)

可以看出来,valid()检查函数是否执行完毕,current()返回当前yield返回的数据,next()使逻辑重新进入函数,并且从进入的yield执行到下一个yield。这样我们就非常明白生成器函数的执行过程了,现在我们就能发现yield给了我们重新进入一个函数的能力,也给力一个函数多次返回的能力,这个函数就是生成器函数。

我们再来理解一下官方文档对生成器语法的说明:

一个生成器函数看起来像一个普通的函数,不同的是普通函数返回一个值,而一个生成器可以yield生成许多它所需要的值。当一个生成器被调用的时候,它返回一个可以被遍历的对象.当你遍历这个对象的时候(例如通过一个foreach循环),PHP 将会在每次需要值的时候调用生成器函数,并在产生一个值之后保存生成器的状态,这样它就可以在需要产生下一个值的时候恢复调用状态。一旦不再需要产生更多的值,生成器函数可以简单退出,而调用生成器的代码还可以继续执行,就像一个数组已经被遍历完了。生成器函数的核心是yield关键字。它最简单的调用形式看起来像一个return申明,不同之处在于普通return会返回值并终止函数的执行,而yield会返回一个值给循环调用此生成器的代码并且只是暂停执行生成器函数。

4.使用

这样的特性给了我们极大的帮助,尤其是大量数据集合的遍历,可以为我们省下很多内存,我们再来看个例子:


  1. <?php
    $n = 999999;
    $startMemory = memory_get_usage();
    $startTime = microtime(true);
    $array = [];
    for ($i = 1; $i <= $n; $i++) {
    $array[] = $i;
    }
    foreach ($array as $a) {
    //do something
    }
    echo "普通方式执行时间:", microtime(true) - $startTime. " ms\n";
    echo "普通方式内存使用:", memory_get_usage() - $startMemory, " bytes\n";
    $startMemory = memory_get_usage();
    $startTime = microtime(true);
    function yieldRange($n) {
    for ($i = 1; $i < $n; $i++) {
    yield $i;
    }
    }
    $gen = yieldRange($n);
    foreach ($gen as $b) {
    //do something
    }
    echo "生成器方式执行时间:", microtime(true) - $startTime. " ms\n"
    echo "生成器方式使用内存:", memory_get_usage() - $startMemory, " bytes\n";

打印结果为:

  1. 普通方式执行时间:0.14968609809875 ms
  2. 普通方式内存使用:33558640 bytes
  3. 生成器方式执行时间:0.77532291412354 ms
  4. 生成器方式使用内存:384 bytes

我使用PHP 7.0.9 (cli)运行的PHP代码,可以看出内存的使用相差非常的大,但是重新进入一个函数的执行流程必定会消耗CPU,所以效率会降低。所以如果内存使用是程序的瓶颈时,应该考虑使用yield的方式来遍历你的大量数据集合。

5.总结

yield的概念,和平时写的顺序执行的代码完全不一样,不能用这种基本的顺序思想来思考。你怎么理解它都可以,可以用两个函数的转换,可以用Iterator的方法,只要你清楚它的执行过程。

yield给了我们重入一个函数的能力,并且Generator也实现了send和throw方法可以和生成器函数通信,这种形式就是协程,其实yield的本质就是协程,yield的作用不只是在遍历大量数据集合上,它把我们引入了协程的世界。

协程的理解

(1)为应用层实现多任务提供了工具; 
(2)协程不允许多任务同时执行,要执行其它协程,必须使用关键字yield主动放弃cpu控制权; 
(3)协程需要自己写任务管理器,以及任务调度器; 
(4)减轻了OS处理零散任务和轻量级任务的负担;



本文由南风码哥发表并编辑,转载此文章须经作者同意,并请附上出处及本页链接。
取消

感谢您的支持,我会继续努力的!

扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦