• 首页
  • vue
  • TypeScript
  • JavaScript
  • scss
  • css3
  • html5
  • php
  • MySQL
  • redis
  • jQuery
  • 生成器总览

    (PHP 5 >= 5.5.0, PHP 7)

    生成器提供了一种更容易的方法来实现简单的对象迭代,相比较定义类实现Iterator接口的方式,性能开销和复杂性大大降低。

    生成器允许你在foreach代码块中写代码来迭代一组数据而不需要在内存中创建一个数组,那会使你的内存达到上限,或者会占据可观的处理时间。相反,你可以写一个生成器函数,就像一个普通的自定义函数一样,和普通函数只返回一次不同的是,生成器可以根据需要yield多次,以便生成需要迭代的值。

    一个简单的例子就是使用生成器来重新实现range()函数。标准的range()函数需要在内存中生成一个数组包含每一个在它范围内的值,然后返回该数组,结果就是会产生多个很大的数组。比如,调用range(0, 1000000)将导致内存占用超过 100 MB。

    做为一种替代方法,我们可以实现一个xrange()生成器,只需要足够的内存来创建Iterator对象并在内部跟踪生成器的当前状态,这样只需要不到1K字节的内存。

    Example #1 将range()实现为生成器

    <?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 ";
    }
    ?>
    

    以上例程会输出:

    Single digit odd numbers from range():  1 3 5 7 9 
    Single digit odd numbers from xrange(): 1 3 5 7 9 
    

    Generator objects

    When a generator function is called for the first time, an object of the internal Generator class is returned. This object implements the Iterator interface in much the same way as a forward-only iterator object would, and provides methods that can be called to manipulate the state of the generator, including sending values to and returning values from it.

    for the protection from the leaking of resources 
    see RFC https://wiki.php.net/rfc/generators#closing_a_generator
    and use finnaly
    sample code
    function getLines($file) {
      $f = fopen($file, 'r');
      try {
        while ($line = fgets($f)) {
          yield $line;
        }
      } finally {
        fclose($f);
      }
    }
    foreach (getLines("file.txt") as $n => $line) {
      if ($n > 5) break;
      echo $line;
    }
    Bear in mind that execution of a generator function is postponed until iteration over its result (the Generator object) begins. This might confuse one if the result of a generator is assigned to a variable instead of immediate iteration.
    <?php
    $some_state = 'initial';
    function gen() {
      global $some_state; 
      echo "gen() execution start\n";
      $some_state = "changed";
      yield 1;
      yield 2;
    }
    function peek_state() {
      global $some_state;
      echo "\$some_state = $some_state\n";
    }
    echo "calling gen()...\n";
    $result = gen();
    echo "gen() was called\n";
    peek_state();
    echo "iterating...\n";
    foreach ($result as $val) {
      echo "iteration: $val\n";
      peek_state();
    }
    ?>
    If you need to perform some action when the function is called and before the result is used, you'll have to wrap your generator in another function.
    <?php
    /**
     * @return Generator
     */
    function some_generator() {
      global $some_state;
      $some_state = "changed";
      return gen();
    }
    ?>
    
    Here's how to detect loop breaks, and how to handle or cleanup after an interruption.
    <?php
      function generator()
      {
        $complete = false;
        try {
          while (($result = some_function())) {
            yield $result;
          }
          $complete = true;
        } finally {
          if (!$complete) {
            // cleanup when loop breaks 
          } else {
            // cleanup when loop completes
          }
        }
        // Do something only after loop completes
      }
    ?>
    
    Same example, different results:
    ----------------------------------
          | time | memory, mb |
    ----------------------------------
    | not gen | 0.7589 | 146.75   |
    |---------------------------------
    | with gen | 0.7469 | 8.75    |
    |---------------------------------
    Time in results varying from 6.5 to 7.8 on both examples.
    So no real drawbacks concerning processing speed.
    Abstract test.
    <?php
    $start_time=microtime(true);
    $array = array();
    $result = '';
    for($count=1000000; $count--;)
    {
     $array[]=$count/2;
    }
    foreach($array as $val)
    {
     $val += 145.56;
     $result .= $val;
    }
    $end_time=microtime(true);
    echo "time: ", bcsub($end_time, $start_time, 4), "\n";
    echo "memory (byte): ", memory_get_peak_usage(true), "\n";
    ?>
    <?php
    $start_time=microtime(true);
    $result = '';
    function it()
    {
     for($count=1000000; $count--;)
     {
      yield $count/2;
     }
    }
    foreach(it() as $val)
    {
     $val += 145.56;
     $result .= $val;
    }
    $end_time=microtime(true);
    echo "time: ", bcsub($end_time, $start_time, 4), "\n";
    echo "memory (byte): ", memory_get_peak_usage(true), "\n";
    ?>
    Result:
    ----------------------------------
          | time | memory, mb |
    ----------------------------------
    | not gen | 2.1216 | 89.25   |
    |---------------------------------
    | with gen | 6.1963 | 8.75    |
    |---------------------------------
    | diff   | < 192% | > 90%   |
    ----------------------------------
    In addition to the note of "montoriusz at gmail dot com": https://www.php.net/manual/en/language.generators.overview.php#119275
    "If you need to perform some action when the function is called and before the result is used, you'll have to wrap your generator in another function."
    You can use Generator::rewind instead (https://www.php.net/manual/en/generator.rewind.php)
    Sample code:
    <?php
    /** function/generator definition **/
    echo "calling gen()...\n";
    $result = gen();
    $result->rewind();
    echo "gen() was called\n";
    /** iteration **/
    ?>
    
    A simple function to parse an ini configuration file
    <?php
      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;
        }
      }
    ?>
    //Usage : parse_ini('param.ini') // returns Generator Object
    //Usage : iterator_to_array(parse_ini('param.ini')); // returns an array
    Same example, different results:
    ----------------------------------
          | time | memory, mb |
    ----------------------------------
    | not gen | 0.7589 | 146.75   |
    |---------------------------------
    | with gen | 0.7469 | 8.75    |
    |---------------------------------
    Time in results varying from 6.5 to 7.8 on both exassmples.
    So no real drawbacks concerning processing speed.

    下篇:生成器语法