美文网首页
PHP生成器yield

PHP生成器yield

作者: 小东班吉 | 来源:发表于2019-07-26 18:09 被阅读0次

    生成器

    生成器提供了一种更容易的方法来实现简单的对象迭代,相比较定义类实现 Iterator 接口的方式,性能开销和复杂性大大降低。
    
    生成器允许你在 foreach 代码块中写代码来迭代一组数据而不需要在内存中创建一个数组, 那会使你的内存达到上限,或者会占据可观的处理时间。相反,你可以写一个生成器函数,就像一个普通的自定义函数一样, 和普通函数只返回一次不同的是, 生成器可以根据需要 yield 多次,以便生成需要迭代的值。
    
    一个简单的例子就是使用生成器来重新实现 range() 函数。 标准的 range() 函数需要在内存中生成一个数组包含每一个在它范围内的值,然后返回该数组, 结果就是会产生多个很大的数组。 比如,调用 range(0, 1000000) 将导致内存占用超过 100 MB。
    
    做为一种替代方法, 我们可以实现一个 xrange() 生成器, 只需要足够的内存来创建 Iterator 对象并在内部跟踪生成器的当前状态,这样只需要不到1K字节的内存。
    

    <?php
    function xrange($start, $limit, $step = 1) {
        if ($start < $limit) {
            if ($step <= 0) {
                throw new LogicException('Step must be +ve');
            }
    
            for ($i = $start; $i <= $limit; $i += $step) {
                yield $i;
            }
        } else {
            if ($step >= 0) {
                throw new LogicException('Step must be -ve');
            }
    
            for ($i = $start; $i >= $limit; $i += $step) {
                yield $i;
            }
        }
    }
    
    /* 
     * 注意下面range()和xrange()输出的结果是一样的。
     */
    
    echo 'Single digit odd numbers from range():  ';
    foreach (range(1, 9, 2) as $number) {
        echo "$number ";
    }
    echo "\n";
    
    echo 'Single digit odd numbers from xrange(): ';
    foreach (xrange(1, 9, 2) as $number) {
        echo "$number ";
    }
    

    生成器的语法

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

    yield关键字

    生成器函数的核心是yield关键字。它最简单的调用形式看起来像一个return申明,不同之处在于普通return会返回值并终止函数的执行,而yield会返回一个值给循环调用此生成器的代码并且只是暂停执行生成器函数。
        
    <?php
    function xrange($start, $end, $step = 1) {
        for ($i = $start; $i <= $end; $i += $step) {
            yield $i;
        }
    }
     
    foreach (xrange(1, 1000000) as $num) {
        echo $num, "\n";
    }
    

    以上例程会输出:1-1000000

    1
    2
    3
    ....
    

    yield关键字,当开始调用的时候返回一个迭代器,而这个迭代器实现了Iterator接口.

    调用迭代器的方法一次, 其中的代码运行一次.例如, 如果你调用$range->rewind(), 那么xrange()里的代码就会运行到控制流第一次出现yield的地方. 而函数内传递给yield语句的返回值可以通过$range->current()获取.
    
    为了继续执行生成器中yield后的代码, 你就需要调用$range->next()方法. 这将再次启动生成器, 直到下一次yield语句出现. 因此,连续调用next()和current()方法, 你就能从生成器里获得所有的值, 直到再没有yield语句出现.
    
    对xrange()来说, 这种情形出现在$i超过$end时. 在这中情况下, 控制流将到达函数的终点,因此将不执行任何代码.一旦这种情况发生,vaild()方法将返回假, 这时迭代结束.
    

    所以也可以用Iterator实现上面的遍历。代码如下:

    // 模拟生成器 rang 函数
    class myIterator implements Iterator {
        private $end   = 1000000;  //end
        private $start = 1;     //start
        private $step  = 1;     //步长
    
        private $position;
    
        public function __construct() {
            $this->position = $this->start;
        }
    
        function rewind() {
            $this->position = $this->start;
        }
    
        function current() {
            return $this->position;
        }
    
        function key() {
            return $this->position;
        }
    
        function next() {
            $this->position += $this->step;
        }
    
        function valid() {
            return $this->position <= $this->end;
        }
    }
    
    $it = new myIterator();
    
    foreach ($it as $key => $value) {
        echo $value . "\n";
    }
    

    以上例程会输出:1-1000000

    1
    2
    3
    ....
    

    再看一个:

    function parse_ini($file_path){
        if(!file_exists($file_path)){
            throw new Exception("File not exists ${file_path}");
        }
        $text = fopen($file_path, 'r');
        while($line=fgets($text)){
            list($key, $param) = explode('=', $line);
            yield $key => $param;
        }
    }
    
    //yield.ini内容为:
    abc = '11111'
    def = '22222'
    ijk = '33333'
    
    foreach ($res = parse_ini('./yield.ini') as $key => $value) {
            var_dump($res->current(),  $res, $key, $value);
            echo "----\n";
            //echo $key .'----'. $value . "\n";
    }
    

    Iterator实现:

    class myParseIniIterator implements Iterator {
        private $text   = null;  //文件流
        private $line   = null;  //一行数据
        public $j = 0;
    
        public function __construct($file_path) {
            $this->text = fopen($file_path, 'r');
        }
    
        private function _lineToArray(){
            list($key, $val) = explode('=', $this->line);
            return ['key' => $key,'val' => $val];
        }
    
        public function rewind() {
            $this->line = fgets($this->text);
        }
    
        public function current() {
            return $this->_lineToArray()['val'];
        }
    
        public function key() {
            return $this->_lineToArray()['key'];
        }
    
        public function next() {
            $this->j++ ;
            if(feof($this->text)) {
                $this->line = fgets($this->text);
                $this->close();
            }else{
                $this->line = fgets($this->text);
            }
        }
    
        public function valid() {
            return !empty($this->line);
        }
    
        public function close() {
            fclose($this->text);
            $this->text = null;
        }
    }
    
    $itparseini = new myParseIniIterator('./yield.ini');
    
    $i = 0;
    foreach ($itparseini as $key => $value) {
    
        echo $key . ' = ' . $value . "\n";
        $i++;
    }
    var_dump($itparseini -> j, $i);exit;
    
    
    class myParseIniIterator implements Iterator {
        private $text   = null;  //文件流
        private $line   = null;  //一行数据
        public $j = 0;
    
        public function __construct($file_path) {
            $this->text = fopen($file_path, 'r');
        }
    
        private function _lineToArray(){
            list($key, $val) = explode('=', $this->line);
            return ['key' => $key,'val' => $val];
        }
    
        public function rewind() {
            $this->line = fgets($this->text);
        }
    
        public function current() {
            return $this->_lineToArray()['val'];
        }
    
        public function key() {
            return $this->_lineToArray()['key'];
        }
    
        public function next() {
            $this->j++ ;
            if(feof($this->text)) {
                $this->line = fgets($this->text);
                $this->close();
            }else{
                $this->line = fgets($this->text);
            }
        }
    
        public function valid() {
            return !empty($this->line);
        }
    
        public function close() {
            fclose($this->text);
            $this->text = null;
        }
    }
    
    $itparseini = new myParseIniIterator('./yield.ini');
    
    $i = 0;
    foreach ($itparseini as $key => $value) {
    
        echo $key . ' = ' . $value . "\n";
        $i++;
    }
    var_dump($itparseini -> j, $i);exit;
    

    协程

    协程的支持是在迭代生成器的基础上, 增加了可以回送数据给生成器的功能(调用者发送数据给被调用的生成器函数). 这就把生成器到调用者的单向通信转变为两者之间的双向通信.

    传递数据的功能是通过迭代器的send()方法实现的. 下面的logger()协程是这种通信如何运行的例子:

    <?php
    function logger($fileName) {
        $fileHandle = fopen($fileName, 'a');
        while (true) {
            fwrite($fileHandle, yield . "\n");
        }
    }
     
    $logger = logger(__DIR__ . '/log');
    $logger->send('Foo');
    $logger->send('Bar')
    

    正如你能看到,这儿yield没有作为一个语句来使用, 而是用作一个表达式, 即它能被演化成一个值. 这个值就是调用者传递给send()方法的值. 在这个例子里, yield表达式将首先被”Foo”替代写入Log, 然后被”Bar”替代写入Log.

    上面的例子里演示了yield作为接受者, 接下来我们看如何同时进行接收和发送的例子:

    <?php
    function gen() {
        $ret = (yield 'yield1');
        var_dump($ret);
        $ret = (yield 'yield2');
        var_dump($ret);
    }
     
    $gen = gen();
    var_dump($gen->current());    // string(6) "yield1"
    var_dump($gen->send('ret1')); // string(4) "ret1"   (the first var_dump in gen)// string(6) "yield2" (the var_dump of the ->send() return value)
    var_dump($gen->send('ret2')); // string(4) "ret2"   (again from within gen) // NULL (the return value of ->send())
    
    
    function gen1() {
        try{
            var_dump(yield 'yield1');
            var_dump(yield 'yield2');
        } catch (Exception $ex) {
            var_dump($ex->getMessage());
        }
    }
    
    $gen = gen1();
    var_dump($gen->current()); //yield1
    var_dum($gen->send("ret1")); //ret1,yield2
    var_dump($gen->throw(new Exception("ret2"))); //ret2, NULL
    ?>
    
    1. 生成迭代对象的时候已经隐含地执行了rewind操作所以$gen->current()的值为yield1,程序此时在yield1处中断.

    2. 当调用$gen->send('ret1')的时候程序恢复执行,从yield1开始执行到yield2又中断,这个时候把传进来的值作为yield1语句的结果,并且恢复执行,所以这个时候yield1语句的返回ret值为ret1,var_dump($gen->send('ret1'))结果为yield2。

    3. 当调用$gen->send('ret2')的时候程序恢复执行,并返回发送的值所以生成器里第二个ret=ret2,下面已经没有可执行的程序了,那么到此结束,所以var_dump($gen->send('ret2'))结果为null

    4. 一定要弄清楚迭代器的执行顺序:

      rewind()-vild()->current->key->next()->vild()->current()->key()->next()

    send的值在这里作为yield的值赋值给ret,然后把本身的值返回。在这里yield为接受者,也属于发送者,先把当前值发送出去,然后中断执行。

    1. 试试下面这个
    function gen() {
        $b = (yield 'foo');
        //var_dump($b);
        $a = (yield 'bar');
        var_dump($a);
    }
    
    $gen = gen();
    var_dump($gen->send('something'));
    

    PHP中协程如何理解?

    这里引用网上看到的一个的答案,说的比较好理解。具体来说,一个包含yeild的php函数,就是协程,他有阶段性的结算值 yield $var, 但是代码并不返回,php的调度者接到这个值后,喂给一个generator,generator是个实现了iterator接口的+和协程通讯接口(比如send方法)的实例,所以可以用在for循环里(另个接口负责和协程通讯)。那么generator收到了这个协程的阶段性的值后,他喂给for循环,等for循环下一次循环的时候,他又启动这个协程,协程从上次中断的点继续执行,继续计算,继续yeild值给generator,generator喂for循环,继续循环,直到协程执行完毕。关于迭代器的运行顺序参考官方文档。
    

    Example #1 这个例子展示了使用 foreach 时,迭代器方法的调用顺序。

    <?php
    class myIterator implements Iterator {
        private $position = 0;
        private $array = array(
            "firstelement",
            "secondelement",
            "lastelement",
        );  
    
        public function __construct() {
            $this->position = 0;
        }
    
        function rewind() {
            var_dump(__METHOD__);
            $this->position = 0;
        }
    
        function current() {
            var_dump(__METHOD__);
            return $this->array[$this->position];
        }
    
        function key() {
            var_dump(__METHOD__);
            return $this->position;
        }
    
        function next() {
            var_dump(__METHOD__);
            ++$this->position;
        }
    
        function valid() {
            var_dump(__METHOD__);
            return isset($this->array[$this->position]);
        }
    }
    
    $it = new myIterator;
    
    foreach($it as $key => $value) {
        var_dump($key, $value);
        echo "\n";
    }
    

    以上例程的输出类似于:

    string(18) "myIterator::rewind"
    string(17) "myIterator::valid"
    string(19) "myIterator::current"
    string(15) "myIterator::key"
    int(0)
    string(12) "firstelement"
    
    string(16) "myIterator::next"
    string(17) "myIterator::valid"
    string(19) "myIterator::current"
    string(15) "myIterator::key"
    int(1)
    string(13) "secondelement"
    
    string(16) "myIterator::next"
    string(17) "myIterator::valid"
    string(19) "myIterator::current"
    string(15) "myIterator::key"
    int(2)
    string(11) "lastelement"
    
    string(16) "myIterator::next"
    string(17) "myIterator::valid"
    
    大概这样:
    rewind->valid->current
    next->valid->current
    

    Table of Contents :

    Iterator::current — 返回当前元素
    Iterator::key — 返回当前元素的键
    Iterator::next — 向前移动到下一个元素
    Iterator::rewind — 返回到迭代器的第一个元素
    Iterator::valid — 检查当前位置是否有效

    相关文章

      网友评论

          本文标题:PHP生成器yield

          本文链接:https://www.haomeiwen.com/subject/czgdrctx.html