PHP的Generator(yield)使用
记录使用yield的一些方法、技巧。
Generator
php文档在这https://www.php.net/manual/zh/class.generator.php
我个人粗暴地理解:Generator是一个机器,yield是Generator的出货口,Generator一次只能生产并存储一个东西,所以会阻塞性的一个一个生产。
Generator内的代码存在两个阻塞继续执行的地方:1:Generator刚生成的时候,它内部代码不会执行 2:运行到yield关键字地方的时候,同样也会阻塞运行。
而正是这个阻塞,才是精髓,它可以让我们交互式的产出一个执行一个,达到节省内存的目的。
而且也很方便把各种循环操作包装成数组一样直接foreach操作
yield
Generator并不能直接new出来,我们要通过执行一个有yield关键字的方法才能获取到Generator。
yield是一个阻塞点,代码执行到yield处的时候就会被阻塞,要调用Generator的一些方法才能往后走。
生成一个Generator
获取一个Generator实例:
function getGenerator(): Generator
{
echo 'start';
yield 1;
yield 2;
for ($i = 3; $i < 10; $i++) {
yield $i;
}
return 'finish';
}
$gen = getGenerator();
//Generator实例生成之后并不会立刻执行,所以此时并不会输出'start'
使用Generator
Generator只能遍历一次,所以每次用完之后想再用就要重新生成Generator
foreach
foreach直接使用,和数组一样
function getGenerator(): Generator
{
echo 'start';
yield 1;
yield 2;
for ($i = 3; $i < 10; $i++) {
yield $i;
}
return 'finish';
}
$gen = getGenerator();
foreach ($gen as $value) {
echo $value;
}
//输出'start123456789'
# 带key
function getGenerator2(): Generator
{
echo 'start';
yield 'one' => 1;
yield 'one' => 2;
for ($i = 3; $i < 10; $i++) {
yield $i => $i;
}
return 'finish';
}
$gen2 = getGenerator2();
foreach ($gen2 as $key => $value) {
echo $key . '=>' . $value,' ';
}
//输出'startone=>1 one=>2 3=>3 4=>4 5=>5 6=>6 7=>7 8=>8 9=>9'
//此处就能看出能用相同key: "one"
调用Generator方法使用
function getGenerator(): Generator
{
echo 'start';
yield 1;
yield 2;
for ($i = 3; $i < 10; $i++) {
yield $i;
}
return 'finish';
}
$gen = getGenerator();
while ($gen->valid()) {
$value = $gen->current();
echo $value;
$gen->next();
}
//输出'start123456789'
方法
介绍下Generator的几个方法,这些方法都是和yield关联
current
current()
有两个作用:
1.如果Generator中的代码没有运行到yield处(其实就是刚生成Generator的时候)就运行到yield处,如果已经没有yield了,就yield一个null。
2.返回yield处的值。
注意此方法不能用于判断迭代是否完成,因为第1点,没有值的时候会yield null
,而在迭代中我们也可能会yield null
rewind
rewind()
一个作用:
1.如果Generator中的代码没有运行到第一个yield处就运行到第一个yield处,如果已经过了第一个yield就直接报错。
主要用来检查迭代前的准备工作,可以看这里https://www.php.net/manual/zh/generator.rewind.php#119121,他贴的代码大概就是清空日志之后进行迭代写入日志的时候才报错,导致新日志没写进去,老日志也被清除了。迭代之前先执行一下这个方法,有错可以及时报错,不要到后面的循环中了再报错。(我感觉current、key、valid方法都能代替这个方法)
valid
valid()
:
1.和current()效果一样,不同的是它只会返回true和false,所以它可以用来判断是否可以继续迭代。
getReturn
getReturn()
:
1.当Generator中的代码执行到return处的时候,可以通过这个方法获取到return出来的东西。
没有执行到return就调用这个方法会报错
key
key()
:
1.和current()效果一样,不过是返回的键,如果没有指定,那就和索引数组一样,从0开始每次迭代往后+1。
next
next()
:
1.如果Generator中的代码没有运行到yield处就先运行到yield处,然后让Generator中的代码执行到下一个yield处
send
send()
:
1.如果Generator中的代码没有运行到yield处就先运行到yield处,send方法传入的值会当作yield的返回值传进去,然后让Generator中的代码执行到下一个yield处并返回值
通过send方法就可以和Generator中动态交互获取值,下面这个例子,用send代替next,可以和生产者进行交互进行:前5个数x2,后面都÷2
function double($i)
{
return $i * 2;
}
function half($i)
{
return $i / 2;
}
function getGenerator(): Generator
{
$op = 'double';
for ($i = 0; $i < 10; $i++) {
$value = $i;
switch ($op) {
case 'half':
$value = half($i);
break;
case 'double':
$value = double($i);
break;
}
$op = yield $value;
}
return 'finish';
}
$gen2 = getGenerator();
while ($gen2->valid()) {
$value = $gen2->current();
$index = $gen2->key();
echo $value, ' ';
if ($index < 5) {
$gen2->send('double');
} else {
$gen2->send('half');
}
}
# 输出'0 2 4 6 8 10 3 3.5 4 4.5 '
throw
throw()
:
1.和send一样,不管这里面传的是Throwable的子级,也就是new一个异常进去,他会在表达式那里抛出异常
实际使用
数据库
cursor
很多PHP框架的模型、DB组件都有一个cursor()方法,这个方法返回的其实就是Generator,看源码很容易看到这几行
while ($record = $statement->fetch()) {
yield $record;
}
这个方法很有用,从数据库读出数据之后,从数据库一条一条拿的,所以一次就能获取符合条件的所有数据之后慢慢从数据库拿,不用重复limit读取增加工作成本。
简单写个极端示例:在后台进程中给所有用户做一些操作(仅是为了展示效果,不适用生产)
//读出来统一发 email数据全部存到内存
$users = Db::table('user')->get();
foreach($users as $user){
sendMail($user->email,'邮件内容xxxxx');//发邮件
sendSms($user->phone);//发短信
giveMoney($user->id);
//......
}
//使用游标一条一条处理 一次读取一条email数据
foreach(Db::table('user')->cursor() as $user){
sendMail($user->email,'邮件内容xxxxx');//发邮件
sendSms($user->phone);//发短信
giveMoney($user->id);
//......
}
上面这个例子夸张一点,几百万数据会很占空间,对数据库也有压力。
chunk
很多PHP框架的模型、DB组件还有一个chunk方法,就是把数据分批读出来,本质语句就是limit offset,但是读一批就要limit一次,还有索引查询、排序等,所以到后面越来越慢,最后可能为了200条数据,排序前面一百多万数据,很多都是重复无用功。这个使用yield套一下,只要稍微改造就可以了。
//原代码
$file = 'log.csv';
$handle = fopen($file, 'w');
Db::table('log')->whereBetween('date', ['2020-01-01', '2024-01-01'])
->where('u_group', 'AZZ9281')
->wher('d_type', '2231')
->orderBy('update_time')
->chunk(200, function ($log) use ($handle) {
foreach ($log as $value) {
fputcsv($handle, (array)$value);
}
});
fclose($handle);
//改造 使用游标加yield套一下
function chunk_yield($query): Generator
{
$data = [];
foreach ($query->cursor() as $row) {
$data[] = $row;
if (count($data) > 100) {
yield $data;
$data = [];
}
}
if ($data) {
yield $data;
}
}
$file = 'log.csv';
$handle = fopen($file, 'w');
$query = Db::table('log')->whereBetween('date', ['2020-01-01', '2024-01-01'])
->where('u_group', 'AZZ9281')
->wher('d_type', '2231')
->orderBy('update_time');
foreach (chunk_yield($query) as $log) {
foreach ($log as $value) {
fputcsv($handle, (array)$value);
}
}
fclose($handle);