记录使用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);

标签: PHP

添加新评论

Loading...
Fullscreen Image