PHP的生成器yield

充阳秋
2023-12-01

yield是什么

生成器是PHP 5.5.0才引入的功能, 生成器函数看上去就像一个普通函数,除了不是返回一个值之外,
生成器会根据需求产生更多的值.

yield特性

  • yield只能用于函数内部,在非函数内部运用会抛出错误.
  • 如果函数包含了yield关键字的,那么函数执行后的返回值永远都是一个Generator对象.
  • 如果函数内部同时包含yield和return,该函数的返回值依然是Generator对象,但是在生成Generator对象时,return语句后的代码被忽略.
  • Generator类实现了Iterator接口.
  • 可以通过返回的Generator对象内部的方法,获取到函数内部yield后面表达式的值.
  • 可以通过Generator的send方法给yield 关键字赋一个值.
  • 一旦返回的Generator对象被遍历完成,便不能调用他的rewind方法来重置
  • Generator对象不能被clone关键字克隆

yield优点

  • 生成器会对PHP应用的性能有非常大的影响
  • PHP代码运行时节省大量的内存
  • 比较适合计算大量的数据

概念引入

  • eg1
        function createRange($number){
            $data = [];
            for($i=0;$i<$number;$i++){
                $data[] = time();
            }
            return $data;
        }
    
        $result = createRange(10); // 这里调用上面我们创建的函数
        foreach($result as $value){
            sleep(1);//这里停顿1秒,我们后续有用
            echo $value.'<br />';
        }
    
  • 代码解读
    • 我们创建一个函数.
    • 函数内包含一个 for 循环,我们循环的把当前时间放到$data里面
    • for循环执行完毕,把 $data 返回出去
    • createRange 函数内的 for 循环结果被很快放到 $data 中,并且立即返回.所以, foreach 循环的是一个固定的数组.
  • eg2
        function createRange($number){
            for($i=0;$i<$number;$i++){
                yield time();
            }
        }
    
        $result = createRange(10); // 这里调用上面我们创建的函数
        foreach($result as $value){
            sleep(1);
            echo $value.'<br />';
        }
    
  • 代码解读
    • createRange函数,传入参数10,但是for值执行了一次然后停止了,并且告诉 foreach 第一次循环可以用的值.
    • foreach开始对$result循环,进来首先 sleep(1) ,然后开始使用 for 给的一个值执行输出.
    • foreach准备第二次循环,开始第二次循环之前,它向for循环又请求了一次.
    • for循环于是又执行了一次,将生成的时间戳告诉foreach .
    • foreach拿到第二个值,并且输出.由于foreach中sleep(1) ,所以,for循环延迟了1秒生成当前时间
    • createRange的值不是一次性快速生成,而是依赖于foreach循环.foreach循环一次,for执行一次.

实际应用

  • 读取超大文件
  • 百万级别的访问量

Iterator接口

会发现当对象被foreach的时候, 内部的valid,current,key方法会依次被调用, 
其返回值便是foreach语句的key和value.循环的终止条件则根据valid方法的返回而定.
如果返回的是true则继续循环, 如果是false则终止整个循环, 结束遍历.当一次循环体结束之后, 
将调用next进行下一次的循环直到valid返回false.而rewind方法则是在整个循环开始前被调用, 
这样保证了多次遍历得到的结果都是一致的
  • 源码
        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 )   //判断当前位置是否有效
        }
    
  • eg
        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";
    

Generator类

  • 源码
        Generator implements Iterator {
            /* 方法 */
            // 返回当前产生的值
            public current ( void ) : mixed
            // 返回当前产生的键
            public key ( void ) : mixed
            // 生成器继续执行
            public next ( void ) : void
            // 重置迭代器
            public rewind ( void ) : void
            // 向生成器中传入一个值
            public send ( mixed $value ) : mixed
            // 向生成器中抛入一个异常
            public throw ( Exception $exception ) : void
            // 检查迭代器是否被关闭
            public valid ( void ) : bool
            // 序列化回调
            public __wakeup ( void ) : void
        }
    

传统解决方法: 内存溢出时报 Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes) 错误,以往通过php.ini或者ini_set() 来设置一个进程的更大内存,虽然能解决大内存问题但不利于接口的性能。

yield原理:假如有100条数据 用foreach就是一次性把数据放到一个数组,内存长度占100。 用yield是一条一条放进去,而且每次放完一条就“销毁”,然后总内存是一条,内存长度只占1。可以想象yield有点类似 return

常见使用场景
用法1,处理一个返回值或一个键值对

// yield
function createRange($number){
    for($i=1;$i<$number;$i++){
        yield $i => time();
    }
}
 
$result = createRange(10); // 这里返回生成器对象(里面是我们要的数组data,但内存只占1)
foreach($result as $k=>$value){
	echo $value.$k;
}

用法2,处理sql查询出来的数组

public static function yieldData($data){
    foreach ($data as $datum){
        yield $datum;
    }
}

$res = Db()->select();
$list = self::yieldData($res);

// 逻辑处理
foreach($list as $k => &$v){}

// 导出等等
Common::csv($list)

用法3,文件读取/导入功能

function readTxt($path)
{
    $handle = fopen($path, 'rb');
    while (feof($handle)===false) {
        yield fgets($handle);
    }
    fclose($handle);
}

foreach (readTxt() as $key => $value) {
   // 逻辑处理
}

总结

yield生成器允许你 在 foreach 代码块中写代码来迭代一组数据,而不需要在内存中创建一个数组,极大提高了进程的内存使用率

 类似资料: