【问题标题】:What does yield mean in PHP?产量在 PHP 中是什么意思?
【发布时间】:2013-07-03 06:10:18
【问题描述】:

我最近偶然发现了这段代码:

function xrange($min, $max) 
{
    for ($i = $min; $i <= $max; $i++) {
        yield $i;
    }
}

我以前从未见过这个yield 关键字。尝试运行我得到的代码

解析错误:语法错误,第 x 行出现意外的 T_VARIABLE

那么yield 关键字是什么?它甚至是有效的PHP吗?如果是,我该如何使用它?

【问题讨论】:

    标签: php generator php-5.5 yield-keyword


    【解决方案1】:

    在实现 PHP IteratorAggregate 接口时,yield 关键字将很有用。查看文档,有几个使用ArrayIteratoryield 的示例。

    另一个例子可以在php-ds/polyfill repo 中找到:https://github.com/php-ds/polyfill/blob/e52796c50aac6e6cfa6a0e8182943027bacbe187/src/Traits/GenericSequence.php#L359

    这个想法类似于下面的快速示例:

    class Collection implements \IteratorAggregate
    {
        private $array = [];
    
        public function push(...$values)
        {
            array_push($this->array, ...$values);
        }
    
        public function getIterator()
        {
            foreach ($this->array as $value) {
                yield $value;
            }
        }
    }
    
    $collection = new Collection();
    $collection->push('apple', 'orange', 'banana');
    
    foreach ($collection as $key => $value) {
        echo sprintf("[%s] => %s\n", $key, $value);
    }
    

    输出:

    [0] => apple
    [1] => orange
    [2] => banana
    

    【讨论】:

      【解决方案2】:

      没有一个答案显示使用由非数字成员填充的大量数组的具体示例。下面是一个使用explode() 生成的数组在一个大的 .txt 文件(我的用例中为 262MB)上的示例:

      <?php
      
      ini_set('memory_limit','1000M');
      
      echo "Starting memory usage: " . memory_get_usage() . "<br>";
      
      $path = './file.txt';
      $content = file_get_contents($path);
      
      foreach(explode("\n", $content) as $ex) {
          $ex = trim($ex);
      }
      
      echo "Final memory usage: " . memory_get_usage();
      

      输出是:

      Starting memory usage: 415160
      Final memory usage: 270948256
      

      现在将其与类似的脚本进行比较,使用 yield 关键字:

      <?php
      
      ini_set('memory_limit','1000M');
      
      echo "Starting memory usage: " . memory_get_usage() . "<br>";
      
      function x() {
          $path = './file.txt';
          $content = file_get_contents($path);
          foreach(explode("\n", $content) as $x) {
              yield $x;
          }
      }
      
      foreach(x() as $ex) {
          $ex = trim($ex);
      }
      
      echo "Final memory usage: " . memory_get_usage();
      

      这个脚本的输出是:

      Starting memory usage: 415152
      Final memory usage: 415616
      

      显然,内存使用量节省相当可观(ΔMemoryUsage -----> ~270.5 MB 在第一个示例中,~450B 在第二个示例中)。

      【讨论】:

        【解决方案3】:

        什么是yield

        yield 关键字 returns data from a generator function:

        生成器函数的核心是 yield 关键字。在最简单的形式中,yield 语句看起来很像 return 语句,只是它不是停止函数的执行并返回,而是为在生成器上循环的代码提供一个值并暂停生成器函数的执行。

        什么是生成器函数?

        生成器函数实际上是编写Iterator 的更紧凑和更有效的方式。它允许您定义一个函数(您的xrange),该函数将计算并返回您是looping over it

        function xrange($min, $max) {
            for ($i = $min; $i <= $max; $i++) {
                yield $i;
            }
        }
        
        […]
        
        foreach (xrange(1, 10) as $key => $value) {
            echo "$key => $value", PHP_EOL;
        }
        

        这将创建以下输出:

        0 => 1
        1 => 2
        …
        9 => 10
        

        您也可以通过使用控制foreach中的$key

        yield $someKey => $someValue;
        

        在生成器函数中,$someKey 是您希望出现的任何内容,$key$someValue$val 中的值。在问题的示例中,$i

        与普通函数有什么区别?

        现在您可能想知道为什么我们不简单地使用 PHP 的原生 range function 来实现该输出。你是对的。输出将是相同的。不同之处在于我们如何到达那里。

        当我们使用range PHP 时,将执行它,在内存中创建整个数字数组并将return整个数组 放到foreach 循环中,然后循环遍历它并输出值。换句话说,foreach 将对数组本身进行操作。 range 函数和 foreach 只“交谈”一次。把它想象成在邮件中收到一个包裹。送货员会把包裹递给你然后离开。然后你打开整个包裹,取出里面的任何东西。

        当我们使用生成器函数时,PHP 会进入该函数并执行它,直到它遇到结尾或yield 关键字。当它遇到yield 时,它会将当时的值返回到外部循环。然后它回到生成器函数并从它产生的地方继续。由于您的 xrange 包含一个 for 循环,因此它将执行并让步,直到达到 $max。把它想象成foreach 和打乒乓球的发电机。

        我为什么需要它?

        显然,生成器可用于解决内存限制。根据您的环境,执行range(1, 1000000) 会使您的脚本致命,而使用生成器同样可以正常工作。或者正如维基百科所说:

        由于生成器仅根据需要计算其产生的值,因此它们对于表示昂贵或不可能一次计算的序列非常有用。这些包括例如无限序列和实时数据流。

        生成器也应该很快。但请记住,当我们谈论快速时,我们通常会谈论非常少的数字。因此,在您现在运行并更改所有代码以使用生成器之前,请先做一个基准测试,看看它在哪里有意义。

        生成器的另一个用例是异步协程。 yield 关键字不仅返回值,而且还接受它们。有关这方面的详细信息,请参阅下面链接的两篇优秀的博客文章。

        什么时候可以使用yield

        PHP 5.5 中引入了生成器。尝试在该版本之前使用yield 会导致各种解析错误,具体取决于关键字后面的代码。因此,如果您从该代码中收到解析错误,请更新您的 PHP。

        来源和进一步阅读:

        【讨论】:

        • 请详细说明yeild 的好处是什么,比如说,像这样的解决方案:ideone.com/xgqevM
        • 啊,好吧,还有我生成的通知。嗯。好吧,我尝试使用辅助类模拟 PHP >= 5.0.0 的生成器,是的,可读性稍差,但我将来可能会使用它。有趣的话题。谢谢!
        • 不是可读性而是内存使用!比较使用的内存以迭代 return range(1,100000000)for ($i=0; $i&lt;100000000; $i++) yield $i
        • @mike 是的,这已经在我的回答中解释过了。在另一个 Mike 的示例中,内存几乎不是问题,因为他只迭代了 10 个值。
        • @Mike xrange 的一个问题是它对静态限制的使用对嵌套很有用(例如在 n 维流形上搜索,或者使用生成器进行递归快速排序)。您不能嵌套 xrange 循环,因为它的计数器只有一个实例。 Yield 版本没有这个问题。
        【解决方案4】:

        这个函数正在使用yield:

        function a($items) {
            foreach ($items as $item) {
                yield $item + 1;
            }
        }
        

        和这个差不多,没有:

        function b($items) {
            $result = [];
            foreach ($items as $item) {
                $result[] = $item + 1;
            }
            return $result;
        }
        

        唯一的区别是a() 返回一个generatorb() 只是一个简单的数组。您可以在两者上进行迭代。

        另外,第一个不分配完整的数组,因此对内存的需求较少。

        【讨论】:

        • 官方文档的补充说明:在 PHP 5 中,生成器无法返回值:这样做会导致编译错误。一个空的 return 语句是生成器中的有效语法,它将终止生成器。从 PHP 7.0 开始,生成器可以返回值,可以使用 Generator::getReturn() 检索这些值。 php.net/manual/en/language.generators.syntax.php
        • 简洁明了。
        【解决方案5】:

        简单示例

        <?php
        echo '#start main# ';
        function a(){
            echo '{start[';
            for($i=1; $i<=9; $i++)
                yield $i;
            echo ']end} ';
        }
        foreach(a() as $v)
            echo $v.',';
        echo '#end main#';
        ?>
        

        输出

        #start main# {start[1,2,3,4,5,6,7,8,9,]end} #end main#
        

        高级示例

        <?php
        echo '#start main# ';
        function a(){
            echo '{start[';
            for($i=1; $i<=9; $i++)
                yield $i;
            echo ']end} ';
        }
        foreach(a() as $k => $v){
            if($k === 5)
                break;
            echo $k.'=>'.$v.',';
        }
        echo '#end main#';
        ?>
        

        输出

        #start main# {start[0=>1,1=>2,2=>3,3=>4,4=>5,#end main#
        

        【讨论】:

        • 那么,它在不中断函数的情况下返回?
        【解决方案6】:

        下面的代码说明了如何使用生成器在完成之前返回结果,这与传统的非生成器方法在完全迭代后返回完整数组不同。使用下面的生成器,值在准备好时返回,无需等待数组完全填充:

        <?php 
        
        function sleepiterate($length) {
            for ($i=0; $i < $length; $i++) {
                sleep(2);
                yield $i;
            }
        }
        
        foreach (sleepiterate(5) as $i) {
            echo $i, PHP_EOL;
        }
        

        【讨论】:

        • 那么,在php中不能使用yield来生成html代码吗?我不知道在真实环境中的好处
        • @GiuseppeLodiRizzini 是什么让你这么认为?
        【解决方案7】:

        值得在这里讨论的一个有趣的方面是通过参考产生。每次我们需要更改参数以使其反映在函数外部时,我们都必须通过引用传递此参数。要将其应用于生成器,我们只需在生成器名称和迭代中使用的变量前面加上一个 & 符号 &amp;

         <?php 
         /**
         * Yields by reference.
         * @param int $from
         */
        function &counter($from) {
            while ($from > 0) {
                yield $from;
            }
        }
        
        foreach (counter(100) as &$value) {
            $value--;
            echo $value . '...';
        }
        
        // Output: 99...98...97...96...95...
        

        上面的示例显示了更改 foreach 循环中的迭代值如何更改生成器中的 $from 变量。这是因为$from通过引用产生,这是由于生成器名称前的 & 符号。因此,foreach 循环中的$value 变量是对生成器函数中$from 变量的引用。

        【讨论】:

        • 你能提到这个策略的好处吗?
        【解决方案8】:

        使用yield,您可以轻松地在单个函数中描述多个任务之间的断点。就是这样,没什么特别的。

        $closure = function ($injected1, $injected2, ...){
            $returned = array();
            //task1 on $injected1
            $returned[] = $returned1;
        //I need a breakpoint here!!!!!!!!!!!!!!!!!!!!!!!!!
            //task2 on $injected2
            $returned[] = $returned2;
            //...
            return $returned;
        };
        $returned = $closure($injected1, $injected2, ...);
        

        如果 task1 和 task2 高度相关,但您需要在它们之间设置断点来执行其他操作:

        • 处理数据库行之间的空闲内存
        • 运行其他任务,这些任务提供对下一个任务的依赖,但通过理解当前代码是无关的
        • 进行异步调用并等待结果
        • 等等...

        然后生成器是最好的解决方案,因为您不必将代码拆分为多个闭包或将其与其他代码混合,或使用回调等...您只需使用yield 添加断点,如果你准备好了,你可以从那个断点继续。

        在没有生成器的情况下添加断点:

        $closure1 = function ($injected1){
            //task1 on $injected1
            return $returned1;
        };
        $closure2 = function ($injected2){
            //task2 on $injected2
            return $returned1;
        };
        //...
        $returned1 = $closure1($injected1);
        //breakpoint between task1 and task2
        $returned2 = $closure2($injected2);
        //...
        

        使用生成器添加断点

        $closure = function (){
            $injected1 = yield;
            //task1 on $injected1
            $injected2 = (yield($returned1));
            //task2 on $injected2
            $injected3 = (yield($returned2));
            //...
            yield($returnedN);
        };
        $generator = $closure();
        $returned1 = $generator->send($injected1);
        //breakpoint between task1 and task2
        $returned2 = $generator->send($injected2);
        //...
        $returnedN = $generator->send($injectedN);
        

        注意:使用生成器很容易出错,所以在实现它们之前一定要编写单元测试! 注意2:在无限循环中使用生成器就像编写一个无限长的闭包......

        【讨论】:

          【解决方案9】:

          yield 关键字用于在 PHP 5.5 中定义“生成器”。 好的,那么generator 是什么?

          来自 php.net:

          生成器提供了一种简单的方法来实现简单的迭代器,而无需实现实现 Iterator 接口的类的开销或复杂性。

          生成器允许您编写使用 foreach 迭代一组数据的代码,而无需在内存中构建数组,这可能会导致您超出内存限制,或者需要大量的处理时间来生成。相反,您可以编写一个与普通函数相同的生成器函数,不同之处在于生成器不是返回一次,而是生成器可以根据需要多次生成,以便提供要迭代的值。

          从这个地方:generators = generators,其他函数(只是一个简单的函数)=functions。

          因此,它们在以下情况下很有用:

          • 你需要做简单的事情(或简单的事情);

            generator 比实现 Iterator 接口要简单得多。另一方面,当然,发电机的功能较少。 compare them.

          • 您需要生成大量数据 - 节省内存;

            实际上,为了节省内存,我们可以通过函数为每次循环迭代生成所需的数据,并在迭代后利用垃圾。所以这里的要点是 - 清晰的代码和可能的性能。看看什么更适合您的需求。

          • 需要生成序列,这取决于中间值;

            这是对先前想法的扩展。与函数相比,生成器可以使事情变得更容易。检查Fibonacci example,并尝试在没有生成器的情况下制作序列。在这种情况下,生成器也可以更快地工作,至少因为将中间值存储在局部变量中;

          • 您需要提高性能。

            在某些情况下,它们的工作速度比功能更快(参见前面的好处);

          【讨论】:

          • 我不明白生成器是如何工作的。这个类实现了迭代器接口。据我所知,迭代器类允许我配置我想如何迭代一个对象。例如 ArrayIterator 获取一个数组或对象,因此我可以在迭代时修改值和键。所以如果迭代器得到整个对象/数组,那么生成器如何不必在内存中构建整个数组???
          猜你喜欢
          • 2011-01-24
          • 2011-01-02
          • 1970-01-01
          • 2010-12-04
          • 2011-08-12
          • 2017-06-11
          • 2018-03-05
          • 2023-03-27
          • 1970-01-01
          相关资源
          最近更新 更多