PHP中生成器学习笔记

之前的一篇文章我们记录了PHP的迭代器(《PHP迭代器学习笔记》),今天进一步了解下PHP的生成器。

一、什么是生成器
先来看看wikipedia关于generator的定义:
In computer science, a generator is a routine that can be used to control the iteration behaviour of a loop. All generators are also iterators.[1] A generator is very similar to a function that returns an array, in that a generator has parameters, can be called, and generates a sequence of values. However, instead of building an array containing all the values and returning them all at once, a generator yields the values one at a time, which requires less memory and allows the caller to get started processing the first few values immediately. In short, a generator looks like a function but behaves like an iterator.

是不是感觉很抽象,我们先看看他在PHP中的具象呈现,然后再回头反复看看上面的定义,你可能就不会那么困惑了。

0、直观概念
生成器在PHP中直观表现是一个自定义的函数,这个函数的功能是遍历对象,往往也叫生成器函数(generator function)。

1、生成器函数例子xrange

function xrange($start, $end, $step = 1) {
    for ($i = $start; $i <= $end; $i += $step) {
        yield $i;
    }
}

foreach (xrange(1, 10) as $num) {
    echo $num, PHP_EOL;
}

2、关于yield关键字
yield最简单的调用形式看起来像一个return申明,不同之处在于普通return会返回值并终止函数的执行,而yield会返回一个值给循环调用此生成器的代码并且只是暂停执行生成器函数。生成器函数的核心是yield关键字。

3、生成器函数和普通函数的区别
3.1、普通函数使用关键字return, 只返回一个单独的值; 生成器函数使用关键字yield, 返回值是以次返回的。

3.2、普通函数return后面的代码不会执行,生成器函数yield后面的代码还会继续执行。

function gen_one_to_three() {
    for ($i = 1; $i <= 3; $i++) {
        // 注意变量$i的值在不同的yield之间是保持传递的。
        yield $i;
        echo 'after yield execute', PHP_EOL;
    }
}

$generator = gen_one_to_three();
foreach ($generator as $value) {
    echo $value, PHP_EOL;
}

以上代码执行结果

1
after yield execute
2
after yield execute
3
after yield execute

3.3、普通函数只能有一个return(多个也可以,但是没啥意义),生成器函数可以有多个yield。

function gen_one_to_three() {
    for ($i = 1; $i <= 3; $i++) {
        // 注意变量$i的值在不同的yield之间是保持传递的。
        yield $i;
        yield $i;
    }
}

$generator = gen_one_to_three();
foreach ($generator as $value) {
    echo $value, PHP_EOL;
}

注:有点儿类似每yield一次生成了单链表的一个节点。

以上代码输出

1
1
2
2
3
3

我们可以看到, 每yield一次,生成了一个值

4、为什么需要生成器函数
生成器函数是一种更容易的遍历对象的方式,相比较迭代器(定义类实现Iterator接口的方式),性能开销和复杂性大大降低。不用再去实现Iterator接口中的5个方法。

二、生成器底层原理
0、生成器底层实际是一个实现Iterator接口的类Generator

Generator implements Iterator {
/* Methods */
public current ( void ) : mixed
public getReturn ( void ) : mixed
public key ( void ) : mixed
public next ( void ) : void
public rewind ( void ) : void
public send ( mixed $value ) : mixed
public throw ( Throwable $exception ) : mixed
public valid ( void ) : bool
public __wakeup ( void ) : void
}

1、我们可以用Generator类中的方法来控制迭代。
还是上面的生成器

function gen_one_to_three() {
    for ($i = 1; $i <= 3; $i++) {
        // 注意变量$i的值在不同的yield之间是保持传递的。
        yield $i;
    }
}

方式一:使用foreach遍历(实际上是foreach内部移动了指针)

$generator = gen_one_to_three();
foreach ($generator as $value) {
    echo $value, PHP_EOL;
    echo 'With current function : ' . $generator->current(), $generator->current(), $generator->current(), PHP_EOL;
}

执行结果

1
With current function : 111
2
With current function : 222
3
With current function : 333

方式二: 使用Generator类中的方法遍历

$gen = gen_one_to_three();

echo $gen->current(), PHP_EOL;
$gen->next();
echo $gen->current(), PHP_EOL;
$gen->next();
echo $gen->current(), PHP_EOL;

执行结果

1
2
3

2、生成器函数如何起作用
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.

三、生成器和调用者的双向通信
上面的例子演示的都是调用者调用生成器函数,数据从生成器传到调用者。调用者还可以通过生成器的send()方法回传数据给生成器(调用者发送数据给被调用的生成器函数),这样就可以实现调用者和生成器之间的双向通信。

0、调用者发送数据到生成器

function logger($fileName) {
    $fileHandle = fopen($fileName, 'a');
    while (true) {
        fwrite($fileHandle, yield . PHP_EOL);
    }
}

$logger = logger(__DIR__ . '/log');
$logger->send('Foo');
$logger->send('Bar');

以上执行结果

cat log

Foo
Bar

说明:send方法的作用就是向 yield 语句处传递一个值。

1、调用者和生成器双向通信

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())

四、生成器使用条件

0、生成器是PHP >= 5.5引入的,需要对应的版本中使用
例如我们在5.4版本中执行上面的生成器函数xrange, 会出现error。

php -v
PHP 5.4.16 (cli) (built: Oct 30 2018 19:30:51)

php xrange.php
PHP Parse error:  syntax error, unexpected '$i' (T_VARIABLE)

1、PHP 7中才能使用yield from

参考:
stackoverflow:What does yield mean in PHP?
PHP手册:生成器

发表评论

电子邮件地址不会被公开。 必填项已用*标注