参考资料

  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版本推出的一个特性,算是比较古老的了,其他很多语言中也有类似的特性存在。但是在实际的项目中,目前用到还比较少。网上相关的文章最出名的就是鸟哥的那篇了,但是都不够细致理解起来较为困难,今天我来给大家超详细的介绍一下这个特性。

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对象被遍历的时候内部是怎么工作的,我将代码改了一下,将接口内的每个方法都尽心输出,借此来窥探一下遍历时对象内部方法的的执行情况。

  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...

看到这里,我相信大家对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, 所以循环到此便终止了。

下面我们用代码来验证一下

    $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目前对我们来说,似乎无太大的用处。当然我们可以利用他来生成一些集合对象,节约一些内存知道数据真正被用到的时候在生成。例如:
我们可以写一个方法

    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语句的值。
首先大家考虑一下下面的代码

    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特性较多也较为繁琐。文章内容难免有出错或者不周全的地方,如果大家发现有错误的地方,也希望大家留言告知, 祝大家周末愉快~

PHP yield 分析,以及协程的实现,超详细版(上)的更多相关文章

  1. 用yield实现python协程

    刚刚介绍了pythonyield关键字,趁热打铁,现在来了解一下yield实现协程. 引用官方的说法: 与线程相比,协程更轻量.一个python线程大概占用8M内存,而一个协程只占用1KB不到内存.协 ...

  2. python yield、yield from与协程

    从生成器到协程 协程是指一个过程,这个过程与调用方协作,产出由调用方提供的值.生成器的调用方可以使用 .send(...)方法发送数据,发送的数据会成为yield表达式的值.因此,生成器可以作为协程使 ...

  3. 00.用 yield 实现 Python 协程

    来源:Python与数据分析 链接: https://mp.weixin.qq.com/s/GrU6C-x4K0WBNPYNJBCrMw 什么是协程 引用官方的说法: 协程是一种用户态的轻量级线程,协 ...

  4. php yield关键字以及协程的实现

    php的yield是在php5.5版本就出来了,而在初级php界却很少有人提起,我就说说个人对php yield的理解 Iterator接口 在php中,除了数组,对象可以被foreach遍历之外,还 ...

  5. yield实现 coroutine协程案例

    yield可以手工实现协程,但python为我们封装了一个greenlet,先看看yield实现,yield需要手工操作,无法实现IO操作时自动切换协程,greenlet是封装好的,能方便使用io切换 ...

  6. 用python实现自己的http服务器——多进程、多线程、协程、单进程非堵塞版、epoll版

    了解http协议 http请求头 GET / HTTP/1.1 Host: www.baidu.com Connection: keep-alive Pragma: no-cache Cache-Co ...

  7. yield支持的协程

    #_author:来童星#date:2019/12/12def consumer(name): print("--->start...") while True: new_b ...

  8. JVM 分析工具和查看命令,超详细

    jinfo 可以输出并修改运行时的java 进程的opts. jps 与unix上的ps类似,用来显示本地的java进程,可以查看本地运行着几个java程序,并显示他们的进程号. jstat 一个极强 ...

  9. Python并发实践_02_通过yield实现协程

    python中实现并发的方式有很多种,通过多进程并发可以真正利用多核资源,而多线程并发则实现了进程内资源的共享,然而Python中由于GIL的存在,多线程是没有办法真正实现多核资源的. 对于计算密集型 ...

随机推荐

  1. Java中对图片文件的类型的获取

    public static void main(String[] args) {        File f = new File("c://test.jpg");         ...

  2. jQuery Ajax跨域问题简易解决方案

    场景:由于业务需求,需要在一个页面显示另外一个页面,并且右键查看源代码看不到一条链接. 实现方式:使用iframe来显示这个首页,至于首页的地址则使用jQuery Ajax来获取.html代码如下: ...

  3. Python学习笔记(二)-Python文件类型及编程模式

    Python环境搭建:linux,Windows... Linux下:[root@localhost StudyPython]# python #进入交互模式Python 2.7.11 (defaul ...

  4. 脱壳第三讲,UPX压缩壳,以及补充壳知识

    脱壳第三讲,UPX压缩壳,以及补充壳知识 一丶什么是压缩壳.以及壳的原理 在理解什么是压缩壳的时候,我们先了解一下什么是壳 1.什么是壳 壳可以简单理解为就是在自己的PE文件中包含了代码.而有不影响我 ...

  5. d3根据数据绘制不同的形状

    绘制力导向图的时候通常节点都是圆形,但也会遇到公司节点绘制成圆型,人绘制成方形的情况,那我们怎么依据数据绘制不同的形状. 你可能首先会想到,这很简单啊,是公司的时候append circle,是人的时 ...

  6. Leetcode题解(29)

    93. Restore IP Addresses 题目 分析:多重循环,判断小数点合适的位置 代码如下(copy网上) class Solution { public: vector<strin ...

  7. 基于winsocket的框体Server和Client

    前面学了一点Winsock的知识,会编写简单的Server和Client,现在就想通过VS2008编写框体的Server和Client,而不是在控制台上的操作了,毕竟学编程就是要多加练习,在实践中发现 ...

  8. 利用VS2008发布一个简单的webservice

    一个开发好的webservice,怎样发布出去,供其他电脑访问呢? 本文将介绍如何发布一个简单的webservice,其中的内容都是在网上查看别人文章,自己仿照着做了一遍,因此,难免会发生错误,如果发 ...

  9. CodeForces - 556A Case of the Zeros and Ones

    //////////////////////////////////////////////////////////////////////////////////////////////////// ...

  10. KMP (next数组的性质及证明)

    性质:如果len%(len-next[len-1])==0,则字符串中必存在最小循环节,且循环次数即为len/(len-next[len-1]); 证明:在前len个字符组成的字符串,存在最小循环节k ...