内存观察
迭代输出一个从1开始一直到10000的数组,步进为1。
随手一写
<?php
$start_mem = memory_get_usage();
$arr = range( 1, 10000 );
foreach( $arr as $item ){
echo $item."\n";
}
$end_mem = memory_get_usage();
fprintf(STDOUT, " use mem : %d bytes \n", $end_mem - $start_mem);
// use mem : 266328 bytes
使用yield
<?php
$start_mem = memory_get_usage();
function yield_range($start, $end): Generator
{
while ($start <= $end) {
$start++;
yield $start;
}
}
foreach (yield_range(0, 9999) as $item) {
echo $item . "\n";
}
$end_mem = memory_get_usage();
fprintf(STDOUT, " use mem : %d bytes \n", $end_mem - $start_mem);
// use mem : 32 bytes
探索
首先,我们观察一下yield_range这个函数跟普通函数不一样的地方,就是普通函数往往都是使用return来返回结果,而这个中则是yield。其次是普通函数中return只能返回一次,这个yield能返回好多次。
那么,我们来分析一波儿这个神奇的yield_range函数。这个yield关键字到底返回的是什么?我们简单看一下:
<?php
function yield_range( $start, $end ){
while( $start <= $end ){
$start++;
yield $start;
}
}
$rs = yield_range( 1, 100 );
var_dump( $rs );
object(Generator)#1 (0) {
}
yield返回的是一个叫做Generator(中文名就是生成器)的object对象,而这个生成器是实现了Iterator接口。
Generator结构
<?php
/**
* @template TKey of array-key
* @template TSend
* @template TReturn
* @template TYield
*
* Generator objects are returned from generators, cannot be instantiated via new.
* @link https://secure.php.net/manual/en/class.generator.php
* @link https://wiki.php.net/rfc/generators
*
* @template-implements Iterator<TKey, TYield>
*/
final class Generator implements Iterator
{
/**
* Throws an exception if the generator is currently after the first yield.
* @return void
*/
public function rewind(): void {}
/**
* Returns false if the generator has been closed, true otherwise.
* @return bool
*/
public function valid(): bool {}
/**
* Returns whatever was passed to yield or null if nothing was passed or the generator is already closed.
* @return TYield
*/
public function current(): mixed {}
/**
* Returns the yielded key or, if none was specified, an auto-incrementing key or null if the generator is already closed.
* @return TKey
*/
#[LanguageLevelTypeAware(['8.0' => 'mixed'], default: 'string|float|int|bool|null')]
public function key() {}
/**
* Resumes the generator (unless the generator is already closed).
* @return void
*/
public function next(): void {}
/**
* Sets the return value of the yield expression and resumes the generator (unless the generator is already closed).
* @param TSend $value
* @return TYield|null
*/
public function send(mixed $value): mixed {}
/**
* Throws an exception at the current suspension point in the generator.
* @param Throwable $exception
* @return TYield
*/
public function PS_UNRESERVE_PREFIX_throw(Throwable $exception): mixed {}
/**
* Returns whatever was passed to return or null if nothing.
* Throws an exception if the generator is still valid.
* @link https://wiki.php.net/rfc/generator-return-expressions
* @return TReturn
* @since 7.0
*/
public function getReturn(): mixed {}
/**
* Serialize callback
* Throws an exception as generators can't be serialized.
* @link https://php.net/manual/en/generator.wakeup.php
* @return void
*/
public function __wakeup() {}
}
所以,既然实现了Iterator接口,所以可以有如下代码:
<?php
function yield_range( $start, $end ){
while( $start <= $end ){
yield $start;
$start++;
}
}
$generator = yield_range( 1, 10 );
// valid() current() next() 都是Iterator接口中的方法
while( $generator->valid() ){
echo $generator->current().PHP_EOL;
$generator->next();
}
重点
这个yield_range函数似乎能够记住它上一次运行到哪儿了,上一次运行的结果是什么,然后紧接着在下一次运行的时候继续从上次终止的地方继续开始。这不是普通的PHP函数可以做得到的!
紧接着,我们需要认识一个生成器对象的一个方法,叫做send,简单看下下面这坨代码:
<?php
function yield_range($start, $end)
{
while ($start <= $end) {
$ret = yield $start;
$start++;
echo "yield receive : " . $ret . PHP_EOL;
}
}
$generator = yield_range(1, 10);
$generator->send(100);
send方法可以修改yield的返回值。如果把send放到foreach中呢。
<?php
function yield_range( $start, $end ){
while( $start <= $end ){
$ret = yield $start;
$start++;
echo "yield receive : ".$ret.PHP_EOL;
}
}
$generator = yield_range( 1, 10 );
foreach( $generator as $item ){
$generator->send( $generator->current() * 10 );
}
yield receive : 10
yield receive :
yield receive : 30
yield receive :
yield receive : 50
yield receive :
yield receive : 70
yield receive :
yield receive : 90
yield receive :
yield的最重要作用就是:自己中断一坨代码的执行,然后主动让出CPU控制权给路人甲;然后又能通过一些方式从刚才中断的地方恢复运行。
<?php
function gen1() {
for( $i = 1; $i <= 10; $i++ ) {
echo "GEN1 : {$i}".PHP_EOL;
sleep( 1 );
yield;
}
}
function gen2() {
for( $i = 1; $i <= 10; $i++ ) {
echo "GEN2 : {$i}".PHP_EOL;
sleep( 1 );
yield;
}
}
$task1 = gen1();
$task2 = gen2();
while( true ) {
echo $task1->current();
echo $task2->current();
$task1->next();
$task2->next();
}
明白了这段代码就明白了 yield 的原理。