PHP yield 分析(详细)

PHP yield 分析


参考资料

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

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, 所以循环到此便终止了。

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章