PHP yield 分析,以及协程的实现,超详细版(上)


参考资料php

  1. http://www.laruence.com/2015/05/28/3038.html
  2. http://php.net/manual/zh/class.generator.php
  3. http://www.cnblogs.com/whoamme/p/5039533.html
  4. http://php.net/manual/zh/class.iterator.php


PHP的 yield 关键字是php5.5版本推出的一个特性,算是比较古老的了,其余不少语言中也有相似的特性存在。可是在实际的项目中,目前用到还比较少。网上相关的文章最出名的就是鸟哥的那篇了,可是都不够细致理解起来较为困难,今天我来给你们超详细的介绍一下这个特性。

html

function gen(){
  while(true){
    yield "gen\n";
  }
}

$gen = gen();

var_dump($gen instanceof Iterator);
echo "hello, world!";

 

若是事先没了解过yield,可能会以为这段代码必定会进入死循环。可是咱们将这段代码直接运行会发现,输出hello, world!,预想的死循环没出现。
到底是什么样的力量,征服了while(true)呢,接下来就带你们一块儿来领略一下yield关键字的魅力。

首先要从foreach提及,咱们都知道对象,数组和对象能够被foreach语法遍历,数字和字符串缺不行。其实除了数组和对象以外PHP内部还提供了一个 Iterator 接口,实现了Iterator接口的对象,也是能够被foreach语句遍历,固然跟普通对象的遍历就很不同了。

如下面的代码为例:数组

class Number implements Iterator{
  protected $key;
  protected $val;
  protected $count;

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

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

  public function next(){
  $this->key += 1;
  $this->val += 2;
  }

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

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

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


foreach (new Number(5) as $key => $value){
  echo "{$key} - {$value}\n";
}

 

这个例子将输出
    1 - 0
    2 - 2
    3 - 4
    4 - 6
    5 - 8

关于上面的number对象,被遍历的过程。若是是初学者,可能会出现有点懵的状况。为了深刻的了解Number对象被遍历的时候内部是怎么工做的,我将代码改了一下,将接口内的每一个方法都尽心输出,借此来窥探一下遍历时对象内部方法的的执行状况。ide

  class Number implements Iterator{  
        protected $i = 1;
        protected $key;
        protected $val;
        protected $count; 
        public function __construct(int $count){
            $this->count = $count;
            echo "第{$this->i}步:对象初始化.\n";
            $this->i++;
        }
        public function rewind(){
            $this->key = 0;
            $this->val = 0;
            echo "第{$this->i}步:rewind()被调用.\n";
            $this->i++;
        }
        public function next(){
            $this->key += 1;
            $this->val += 2;
            echo "第{$this->i}步:next()被调用.\n";
            $this->i++;
        }
        public function current(){
            echo "第{$this->i}步:current()被调用.\n";
            $this->i++;
            return $this->val;
        }
        public function key(){
            echo "第{$this->i}步:key()被调用.\n";
            $this->i++;
            return $this->key;
        }
        public function valid(){
            echo "第{$this->i}步:valid()被调用.\n";
            $this->i++;
            return $this->key < $this->count;
        }
    }

    $number = new Number(5);
    echo "start...\n";
    foreach ($number as $key => $value){
        echo "{$key} - {$value}\n";
    }
    echo "...end...\n";

 

以上代码输出以下函数

第1步:对象初始化.
start...
第2步:rewind()被调用.
第3步:valid()被调用.
第4步:current()被调用.
第5步:key()被调用.
0 - 0
第6步:next()被调用.
第7步:valid()被调用.
第8步:current()被调用.
第9步:key()被调用.
1 - 2
第10步:next()被调用.
第11步:valid()被调用.
第12步:current()被调用.
第13步:key()被调用.
2 - 4
第14步:next()被调用.
第15步:valid()被调用.
第16步:current()被调用.
第17步:key()被调用.
3 - 6
第18步:next()被调用.
第19步:valid()被调用.
第20步:current()被调用.
第21步:key()被调用.
4 - 8
第22步:next()被调用.
第23步:valid()被调用.
...end...
View Code


看到这里,我相信你们对Iterator接口已经有必定认识了。会发现当对象被foreach的时候,内部的valid,current,key方法会依次被调用,其返回值即是foreach语句的key和value。循环的终止条件则根据valid方法的返回而定。若是返回的是true则继续循环,若是是false则终止整个循环,结束遍历。当一次循环体结束以后,将调用next进行下一次的循环直到valid返回false。而rewind方法则是在整个循环开始前被调用,这样保证了咱们屡次遍历获得的结果都是一致的。

那么这个跟yield有什么关系呢,这即是咱们接下来要说的重点了。首先给你们介绍一下我总结出来的 yield 的特性,包含如下几点。
1.yield只能用于函数内部,在非函数内部运用会抛出错误。
2.若是函数包含了yield关键字的,那么函数执行后的返回值永远都是一个Generator对象。
3.若是函数内部同事包含yield和return 该函数的返回值依然是Generator对象,可是在生成Generator对象时,return语句后的代码被忽略。
4.Generator类实现了Iterator接口。
5.能够经过返回的Generator对象内部的方法,获取到函数内部yield后面表达式的值。
6.能够经过Generator的send方法给yield 关键字赋一个值。
7.一旦返回的Generator对象被遍历完成,便不能调用他的rewind方法来重置
8.Generator对象不能被clone关键字克隆

首先看第1点,能够明白咱们文章开头的gen函数执行后返回的是一个Generatory对象,因此代码能够继续执行下去输出hello, world!,所以$gen是一个Generator对象,因为其实现了Iterator,因此这个对象能够被foreach语句遍历。下面咱们来看看对其进行遍历,会是什么样的效果。为了防止被死循环,我加多了一个break语句只进行十次循环,方便咱们了解yield的一些特性。
代码以下:性能

    $i = 0;
    foreach ($gen as $key => $value) {
        echo "{$key} - {$value}";
        if(++$i >= 10){
            break;
        }
    }

 


以上代码输出为
    0 - gen
    1 - gen
    2 - gen
    3 - gen
    4 - gen
    5 - gen
    6 - gen
    7 - gen
    8 - gen
    9 - gen
经过观察不难发现其中的规律。在包含yield的函数返回的对象被foreach遍历时, 函数体内部的代码会被对应的执行。PHP 会分析其内部的代码从而生成对应的Iterator接口的方法。
其中key方法实现是返回的是yield出现的次序,从0开始递增。
current方法则是yield后面表达式的值。
而valid方法则在当前yield语句存在的时候返回true, 若是当前不在yield语句的时候返回false。
next方法则执行从当前到下一个yield、或者return、或者函数结束之间的代码。
网上也有文章让你们把yield理解为暂时中止函数的执行,等待外部的激活从而再次执行。虽然看起来确实像那么回事,但我不建议你们这么理解,由于他自己是返回一个迭代器对象,其返回值是能够被用于迭代的。咱们理解了他被foreach迭代时,其内部是如运做的以后更易于理解yield关键字的本质。
下面咱们再作一个简单的测试,以便更直观的展现他的特性。测试

    function gen1(){
        yield 1;
        echo "i\n";
        yield 2;
        yield 3+1;
    }
    $gen = gen1();
    foreach ($gen as $key => $value) {
        echo "{$key} - {$value}\n";
    }

 

以上的代码输出
    0 - 1
    i
    1 - 2
    2 - 4
咱们来分析一下输出的结果,首先当遍历开始时rewind被执行因为第一个yield以前无任何语句,无任何输出。
key的值为yield出现的次序为0,current为yield表达式后的值也就是1。
foreach开始,valid由于当前为第一个yield,因此返回true。正常输出0 - 1
此时next方法被执行,跳转到了第二个yield,第一个到第二个之间的代码被执行输出了i。
再次进入循环 执行vaild,因为当前在第二个yield上面,因此依然是true
因为next执行了,因此key的值也有刚刚的0变为了1,current的值为2,正常输出 1 - 2。
这时候继续执行next(),进入循环vaild()执行,因为此时到了第三个yield返回依然是true。key的值为2, yield为4。正常输出 2 - 4
再次执行next(),因为后续没有yield了vaild()返回为false, 因此循环到此便终止了。

下面咱们用代码来验证一下this

    $gen = gen1();
    var_dump($gen->valid());
    echo $gen->key().' - '.$gen->current()."\n";
    $gen->next(); 
    var_dump($gen->valid());
    echo $gen->key().' - '.$gen->current()."\n";
    $gen->next(); 
    var_dump($gen->valid());
    echo $gen->key().' - '.$gen->current()."\n";
    $gen->next(); 
    var_dump($gen->valid());


输出值以下
    bool(true)
    0 - 1
    i
    bool(true)
    1 - 2
    bool(true)
    2 - 4
    bool(false)
跟咱们的分析彻底一致,至此咱们了解了Iterator接口在遍历时内部的运做方式,也了解了包含yield关键字的函数所生成的对象内部是如何实现Iterator接口的方法的。对于yild的特性了解一半了,可是若是咱们仅仅将其用于生成能够被遍历的对象的话,yield目前对咱们来讲,彷佛无太大的用处。固然咱们能够利用他来生成一些集合对象,节约一些内存知道数据真正被用到的时候在生成。例如:
咱们能够写一个方法spa

    function gen2(){
        yield getUserData();
        yield getBannerList();
        yield getContext();
    }
    #中间其余操做
    #而后在view中得到数据
    $data = gen2();
    foreach ($data as $key => $value) {
        handleView($key, $value);
    }

 


经过以上的代码,咱们将几个获取数据的操做都延迟到了数据被渲染的时候执行。节省了中间进行其余操做时获取回来的数据占用的内存空间。然而实际开放项目的过程当中,这些数据每每被多处使用。并且这样的结构让咱们单独控制数据变得艰难,以此带来的性能提高相对于便利性来讲,好处微乎其微。不过还好的是,咱们对yield的了解才刚刚到一半,已经有这样的功效了。相信咱们在了解完另一半以后,它的功效将大大提高。
接下来咱们来继续了解yield, 因为yield返回的是一个Generator类的对象,这个对象除了实现了Iterator接口以外,内部还有一个至关重要的方法就是send方法,即咱们提到的第6点特性,经过send方法咱们能够给yield发送一个值做为yield语句的值。
首先你们考虑一下下面的代码.net

    function gen3(){
        echo "test\n";
        echo (yield 1)."I\n";
        echo (yield 2)."II\n";
        echo (yield 3 + 1)."III\n";
    }
    $gen = gen3();
    foreach ($gen as $key => $value) {
        echo "{$key} - {$value}\n";
    }


执行之后输出
    0 - 1
    I
    1 - 2
    II
    2 - 4
    III
可能这段输出比较难理解,咱们接下来,一步一步分析一下为何得出这样的输入。因为咱们知道了foreach的时候gen内部是如何操做的,那么咱们便用代码来实现一次。

    $gen = gen3();
    $gen->rewind();
    echo $gen->key().' - '.$gen->current()."\n"; 
    $gen->next(); 

执行后输出
    0 - 1
    I
经过这两句咱们发现,当前的key为0,current则为1也就是yield后面表达式的值。由于yield 1被括号括起来了,因此yield后面表达式的值是1,若是没有括号则为1."I\n".固然由于1."I\n"是一个错误语法。若是想要测试的朋友须要给1加上双引号。
当执行next时,第1个yield到第二个yieldz之间的的语法被执行。也就是echo (yield 1)."I\n"被执行了,因为咱们使用的是next(),因此yield当前是无值的。因此输出了I。须要注意的是在第一个yield以后的语法将不会被执行,而 echo (yield 2). "II\n";属于下一个yield块的语句,因此不会被执行。
到这里,是时候让咱们今天最后的主角send方法来表现一下了。

public mixed Generator::send ( mixed $value )
这个是手册里send方法的描述,能够看出来他能够接受一个mixed类型的参数,也会返回一个mixed类型的值。
传入的参数会被作 yield 关键字在语句中的值,而他的返回值则是next以后,$gen->current()的值。

下面咱们来尝试一下

    $gen = gen3(); 
    $gen->rewind();
    echo $gen->key().' - '.$gen->current()."\n"; 
    echo $gen->send("send value - ");  

执行后输出
    0 - 1
    send value - I
    2
这时候咱们发现,咱们经过send方法成功的将一个值传递给了一个函数的内部,而且当作yield关键字的值给输出了,因为下一个yield的值为2,因此咱们调用send返回的值为2,一样被输出。

虽然咱们知道了send能够完成内部对函数内部的yield表达式传值,也知道了能够经过$gen->current()得到当前yield表达式以后的值,可是这个有什么用呢。能够看一下这个函数

    function gen4(){
        $id = 2;
        $id = yield $id;
        echo $id;
    }

    $gen = gen4();
    $gen->send($gen->current() + 3);

根据上面对yield代码的理解,咱们不难发现这个函数会输出5,由于current()为2,而当咱们send以后 yield的值为 2 + 3,也就是5.同时yield到函数结束之间的代码被执行。也就是$id = 5; echo $id; 经过这样一个简单的例子,咱们发现。咱们不但从函数内部得到了返回值,而且将他的返回值再次发送给了函数内部参与后续的计算。 关于yield的介绍就到此为止了,本文至此也告一段落。后续将会给你们带来,关于yield的下篇,实现一个调度器使得咱们只须要将gen()函数返回的gen对象传递给调度器,其内部的代码就能自动的执行。而且让利用yield来实现并行(伪),以及在多个$gen对象执行之间创建联系和控制其执行顺序,请你们多多关注。另外因为本人才疏学浅,yield特性较多也较为繁琐。文章内容不免有出错或者不周全的地方,若是你们发现有错误的地方,也但愿你们留言告知, 祝你们周末愉快~

相关文章
相关标签/搜索