|
| 1 | +其实,我并不是因为迭代或者生成器或者研究PHP手册才认识的yield,要不是协程,我到现在也不知道PHP中还有yield这么个鬼东西。人家这个东西是从PHP 5.5就开始引入了,官方名称叫做生成器。你要说为什么5.5年代的东西,现在才拿出来。我还想问你哟,PHP 5.3就有了的namespace为毛到最近这几年才开始正式投产。 |
| 2 | + |
| 3 | +那么,问题来了,这东西到底是有何用? |
| 4 | + |
| 5 | +先来感受一个问题,给你100Kb的内存(是的,你没有看错,就是100Kb),然后让你迭代输出一个从1开始一直到10000的数组,步进为1。 |
| 6 | + |
| 7 | +愈先迭代数组,必先创造数组。 |
| 8 | + |
| 9 | +所以,脑门一拍,代码一坨如下: |
| 10 | + |
| 11 | +```php |
| 12 | +<?php |
| 13 | +$start_mem = memory_get_usage(); |
| 14 | +$arr = range( 1, 10000 ); |
| 15 | +foreach( $arr as $item ){ |
| 16 | + //echo $item.','; |
| 17 | +} |
| 18 | +$end_mem = memory_get_usage(); |
| 19 | +echo " use mem : ". ( $end_mem - $start_mem ) .'bytes'.PHP_EOL; |
| 20 | +``` |
| 21 | + |
| 22 | +一顿操作猛如虎,运行一下成绩1-5,你们感受一下: |
| 23 | + |
| 24 | + |
| 25 | + |
| 26 | +528440bytes,约莫就是528Kb,几乎是100Kb的五倍了,妈的这日子没法过了。 |
| 27 | + |
| 28 | + |
| 29 | + |
| 30 | +毕竟你们也知道,最近内存价格确实贵,国家也在号召低碳节能减排,你多耗费5倍内存,就意味着多排放5倍的二氧化碳,就意味着要为多用的内存多花钱贡献给棒子... ...你想想,那可是棒子。 |
| 31 | + |
| 32 | +人都是被逼出来的,于是yield可以来救场了,大概代码如下,注意看操作: |
| 33 | + |
| 34 | +```php |
| 35 | +<?php |
| 36 | +$start_mem = memory_get_usage(); |
| 37 | +function yield_range( $start, $end ){ |
| 38 | + while( $start <= $end ){ |
| 39 | + $start++; |
| 40 | + yield $start; |
| 41 | + } |
| 42 | +} |
| 43 | +foreach( yield_range( 0, 9999 ) as $item ){ |
| 44 | + echo $item.','; |
| 45 | +} |
| 46 | +$end_mem = memory_get_usage(); |
| 47 | +echo " use mem : ". ( $end_mem - $start_mem ) .'bytes'.PHP_EOL; |
| 48 | +``` |
| 49 | + |
| 50 | +运行一下,你们感受一下: |
| 51 | + |
| 52 | + |
| 53 | + |
| 54 | +首先,我们观察一下yield_range这个函数跟普通函数不一样的地方,就是普通函数往往都是使用return来返回结果,而这个中则是yield。其次是普通函数中return只能返回一次,这个yield能返回好多次。 |
| 55 | + |
| 56 | +那么,我们来分析一波儿这个神奇的yield_range函数。这个yield关键字到底返回的是什么?我们简单看一下: |
| 57 | + |
| 58 | +```php |
| 59 | +<?php |
| 60 | +function yield_range( $start, $end ){ |
| 61 | + while( $start <= $end ){ |
| 62 | + $start++; |
| 63 | + yield $start; |
| 64 | + } |
| 65 | +} |
| 66 | +$rs = yield_range( 1, 100 ); |
| 67 | +var_dump( $rs ); |
| 68 | +/* |
| 69 | +object(Generator)#1 (0) { |
| 70 | +} |
| 71 | +*/ |
| 72 | +``` |
| 73 | + |
| 74 | +yield返回的是一个叫做Generator(中文名就是生成器)的object对象,而这个生成器是实现了Iterator接口(至于Iterator接口,你们去PHP手册上搜索吧)。所以,既然实现了Iterator接口(也正是因为如此,这个东西可以使用foreach进行迭代,明白了吧?),所以可以有如下代码: |
| 75 | + |
| 76 | +```php |
| 77 | +<?php |
| 78 | +function yield_range( $start, $end ){ |
| 79 | + while( $start <= $end ){ |
| 80 | + yield $start; |
| 81 | + $start++; |
| 82 | + } |
| 83 | +} |
| 84 | +$generator = yield_range( 1, 10 ); |
| 85 | +// valid() current() next() 都是Iterator接口中的方法 |
| 86 | +while( $generator->valid() ){ |
| 87 | + echo $generator->current().PHP_EOL; |
| 88 | + $generator->next(); |
| 89 | +} |
| 90 | +``` |
| 91 | + |
| 92 | +运行结果如下所示: |
| 93 | + |
| 94 | + |
| 95 | + |
| 96 | +重点来了:这个yield_range函数似乎能够记住它上一次运行到哪儿了,上一次运行的结果是什么,然后紧接着在下一次运行的时候继续从上次终止的地方继续开始。这不是普通的PHP函数可以做得到的! |
| 97 | + |
| 98 | +我们知道,操作系统在调度进程的时候,会触发一个叫做“进程上下文切换”的概念。比如CPU从进程A调度给进程B了,那么当再次从进程B调度给进程A的时候,当初进程A运行到哪儿了、临时的数据结果是什么都是需要被还原的,不然,一切都要从头,那就要出大问题了。而,这个yield关键字,似乎在用户态(非系统内核级)就可以实现这个概念。所以说,用yield搞迭代,怕是真的很没出息的一件事,它能做的太多。 |
| 99 | + |
| 100 | +紧接着,我们需要认识一个生成器对象的一个方法,叫做send,简单看下下面这坨代码: |
| 101 | + |
| 102 | +```php |
| 103 | +<?php |
| 104 | +function yield_range( $start, $end ){ |
| 105 | + while( $start <= $end ){ |
| 106 | + $ret = yield $start; |
| 107 | + $start++; |
| 108 | + echo "yield receive : ".$ret.PHP_EOL; |
| 109 | + } |
| 110 | +} |
| 111 | +$generator = yield_range( 1, 10 ); |
| 112 | +$generator->send( $generator->current() * 10 ); |
| 113 | +``` |
| 114 | + |
| 115 | +运行结果如图所示: |
| 116 | + |
| 117 | + |
| 118 | + |
| 119 | +send方法可以修改yield的返回值,但是,你也不能想当然,比如下面这坨代码,你们以为运行结果是什么样呢? |
| 120 | + |
| 121 | +```php |
| 122 | +<?php |
| 123 | +function yield_range( $start, $end ){ |
| 124 | + while( $start <= $end ){ |
| 125 | + $ret = yield $start; |
| 126 | + $start++; |
| 127 | + echo "yield receive : ".$ret.PHP_EOL; |
| 128 | + } |
| 129 | +} |
| 130 | +$generator = yield_range( 1, 10 ); |
| 131 | +foreach( $generator as $item ){ |
| 132 | + $generator->send( $generator->current() * 10 ); |
| 133 | +} |
| 134 | +``` |
| 135 | + |
| 136 | +本来以为运行结果是类似于这样的: |
| 137 | + |
| 138 | +```php |
| 139 | +<?php |
| 140 | +yield receive : 10 |
| 141 | +yield receive : 20 |
| 142 | +yield receive : 30 |
| 143 | +yield receive : 40 |
| 144 | +yield receive : 50 |
| 145 | +yield receive : 60 |
| 146 | +yield receive : 70 |
| 147 | +yield receive : 80 |
| 148 | +yield receive : 90 |
| 149 | +yield receive : 100 |
| 150 | +``` |
| 151 | + |
| 152 | +然而,唯物主义告诉我们: |
| 153 | + |
| 154 | + |
| 155 | + |
| 156 | +结果是打脸的,你们感受一下: |
| 157 | + |
| 158 | + |
| 159 | + |
| 160 | +为什么我把php版本信息什么的打印出来呢?因为,这是个bug,这是个php的bug,至少我正在使用的PHP 7.1.17版本是有这个bug的,你不要以为这里面有什么高深莫测的技术,就是bug而已。下面是bug链接,你们可以去观摩一下: |
| 161 | + |
| 162 | +https://bugs.php.net/bug.php?id=76104 |
| 163 | +https://stackoverflow.com/questions/37817315/how-does-generatorsend-work |
| 164 | + |
| 165 | +总结一句话,就是不要在foreach中使用生成器的send方法。 |
| 166 | + |
| 167 | +然而,我在国内的一些有关php yield的文章中,都没有看到有人提及这个bug,我坑我自己是淌过了,你们是没必要再淌了。 |
0 commit comments