php yield关键字

        如果是做Python或者其他语言的小伙伴,对于生成器应该不陌生。但很多PHP开发者或许都不知道生成器这个功能,可能是因为生成器是PHP 5.5.0才引入的功能,也可以是生成器作用不是很明显。但是,生成器功能的确非常有用。

        iterator迭代器 

        generator 生成器

优点

直接讲概念估计你听完还是一头雾水,所以我们先来说说优点,也许能勾起你的兴趣。那么生成器有哪些优点,如下:

  • 生成器会对PHP应用的性能有非常大的影响

  • PHP代码运行时节省大量的内存

  • 比较适合计算大量的数据

那么,这些神奇的功能究竟是如何做到的?我们先来举个例子。

概念引入

首先,放下生成器概念的包袱,来看一个简单的PHP函数:

function createRange($number){
    $data = [];
    for($i=0;$i<$number;$i++){
        $data[] = time();
    }
    return $data;
}

这是一个非常常见的PHP函数,我们在处理一些数组的时候经常会使用。这里的代码也非常简单:

  1. 我们创建一个函数。

  2. 函数内包含一个for循环,我们循环的把当前时间放到$data里面

  3. for循环执行完毕,把$data返回出去。

下面没完,我们继续。我们再写一个函数,把这个函数的返回值循环打印出来:

$result = createRange(10); // 这里调用上面我们创建的函数
foreach($result as $value){
    sleep(1);//这里停顿1秒,我们后续有用
    echo $value.'<br />';
}

我们在浏览器里面看一下运行结果:

冷暖自知一抹茶ck

这里非常完美,没有任何问题。(当然sleep(1)效果你们看不出来)

思考一个问题

我们注意到,在调用函数createRange的时候给$number的传值是10,一个很小的数字。假设,现在传递一个值10000000(1000万)。

那么,在函数createRange里面,for循环就需要执行1000万次。且有1000万个值被放到$data里面,而$data数组在是被放在内存内。所以,在调用函数时候会占用大量内存。

这里,生成器就可以大显身手了。

创建生成器

我们直接修改代码,你们注意观察:

function createRange($number){
    for($i=0;$i<$number;$i++){
        yield time();
    }
}

看下这段和刚刚很像的代码,我们删除了数组$data,而且也没有返回任何内容,而是在time()之前使用了一个关键字yield

使用生成器

我们再运行一下第二段代码:

$result = createRange(10); // 这里调用上面我们创建的函数
foreach($result as $value){
    sleep(1);
    echo $value.'<br />';
}

冷暖自知一抹茶ck

我们奇迹般的发现了,输出的值和第一次没有使用生成器的不一样。这里的值(时间戳)中间间隔了1秒。

这里的间隔一秒其实就是sleep(1)造成的后果。但是为什么第一次没有间隔?那是因为:

  • 未使用生成器时:createRange函数内的for循环结果被很快放到$data中,并且立即返回。所以,foreach循环的是一个固定的数组。

  • 使用生成器时:createRange的值不是一次性快速生成,而是依赖于foreach循环。foreach循环一次,for执行一次。

到这里,你应该对生成器有点儿头绪。

概念理解

到这里,你应该已经大概理解什么是生成器了。下面我们来说下生成器原理。

首先明确一个概念:生成器yield关键字不是返回值,他的专业术语叫产出值,只是生成一个值

那么代码中foreach循环的是什么?其实是PHP在使用生成器的时候,会返回一个Generator类的对象。foreach可以对该对象进行迭代,每一次迭代,PHP会通过Generator实例计算出下一次需要迭代的值。这样foreach就知道下一次需要迭代的值了。

而且,在运行中for循环执行后,会立即停止。等待foreach下次循环时候再次和for索要下次的值的时候,for循环才会再执行一次,然后立即再次停止。直到不满足条件不执行结束。

实际开发应用

很多PHP开发者不了解生成器,其实主要是不了解应用领域。那么,生成器在实际开发中有哪些应用?

读取超大文件

PHP开发很多时候都要读取大文件,比如csv文件、text文件,或者一些日志文件。这些文件如果很大,比如5个G。这时,直接一次性把所有的内容读取到内存中计算不太现实。

这里生成器就可以派上用场啦。简单看个例子:读取text文件

冷暖自知一抹茶ck

我们创建一个text文本文档,并在其中输入几行文字,示范读取。

<?php
header("content-type:text/html;charset=utf-8");
function readTxt()
{
    # code...
    $handle = fopen("./test.txt", 'rb');

    while (feof($handle)===false) {
        # code...
        yield fgets($handle);
    }

    fclose($handle);
}

foreach (readTxt() as $key => $value) {
    # code...
    echo $value.'<br />';
}

冷暖自知一抹茶ck

通过上图的输出结果我们可以看出代码完全正常。

但是,背后的代码执行规则却一点儿也不一样。使用生成器读取文件,第一次读取了第一行,第二次读取了第二行,以此类推,每次被加载到内存中的文字只有一行,大大的减小了内存的使用。

这样,即使读取上G的文本也不用担心,完全可以像读取很小文件一样编写代码。


        在php中,除了数组,对象可以被foreach遍历之外,还有另外一种特殊对象,也就是继承了iterator接口的对象,也可以被对象遍历,但和普通对象的遍历又有所不同.

class Interatorobj implements Iterator
{
    protected $data;
    public function __construct($array)
    {
        $this->data = $array;
    }

    public function rewind()
    {
        // TODO: Implement rewind() method.
        echo "指针重置\n";
        reset($this->data);
    }
    public function current()
    {
        // TODO: Implement current() method.
        echo "当前指针数据\n";
        return current($this->data). "自定义遍历值";
    }
    public function next()
    {
        // TODO: Implement next() method.
        echo "指针下移\n";
        next($this->data);
    }
    public function key()
    {
        // TODO: Implement key() method.
        echo "返回当前数组键值\n";
        return key($this->data);
    }
    public function valid()
    {
        // TODO: Implement valid() method.
        echo "检查迭代器指针是否正常\n";
        return current($this->data);
    }
}
foreach (new Interatorobj( array(
    'a'=>1,
    'b'=>2,
    'c'=>3,
)) as $key=>$value)
{
    echo "迭代器自定义遍历\n";
    var_dump($key,$value);
}

冷暖自知一抹茶ck

        可以看出,迭代器的遍历,会依次调用重置,检查当前数据,返回当前指针数据,指针下移方法,结束遍历的条件在于检查数据返回true或者false

生成器

        生成器和迭代器类似,但也完全不同

        生成器允许你在 foreach 代码块中写代码来迭代一组数据而不需要在内存中创建一个数组, 那会使你的内存达到上限,或者会占据可观的处理时间。相反,你可以写一个生成器函数,就像一个普通的自定义函数一样, 和普通函数只返回一次不同的是, 生成器可以根据需要 yield 多次,以便生成需要迭代的值。

        生成器使用yield关键字进行生成迭代的值

function xrange($start, $limit ,$step)
{
    if($start < $limit)
    {
        if($step <= 0)
        {
            throw new LogicException("step 必须大于0");
        }
        else
        {
            for ($i=$start; $i<=$limit; $i+=$step)
            {
                yield $i;
               //break;    //只遍历一次对象
            }
        }
    }
    else
    {
        if($step >= 0)
        {
            throw new LogicException("step 必须小于0");
        }
        else
        {
            for ($i=$start; $i>=$limit; $i+=$step)
            {
                yield $i;
            }
        }
    }
}
var_dump(xrange(1,9,2));
foreach (xrange(1,9,2) as $key=>$value)
{
    var_dump($value);
}

输出结果:

object(Generator)#1 (0) {
}
int(1)
int(3)
int(5)
int(7)
int(9)

进程已结束,退出代码为 0

一:生成器方法

生成器它的内部实现了以下方法:

Generator implements Iterator {    
	//返回当前产生的值 
	public mixed current ( void )

	//返回当前产生的键 
	public mixed key ( void )

	//生成器继续执行 
	public void next ( void )

	//重置迭代器,如果迭代已经开始了,这里会抛出一个异常。 
	public void rewind ( void )

	//向生成器中传入一个值,当前yield接收值,然后继续执行下一个yield 
	public mixed send ( mixed $value )

	//向生成器中抛入一个异常 
	public void throw ( Exception $exception )

	//检查迭代器是否被关闭,已被关闭返回 FALSE,否则返回 TRUE 
	public bool valid ( void )

	//序列化回调 
	public void __wakeup ( void )

	//返回generator函数的返回值,PHP version 7+ 
	public mixed getReturn ( void ) 
}


二:语法

生成器的语法有很多种用法,需要一一说明,首先,yield必须有函数包裹,包裹yield的函数称为"生成器函数",该函数将返回一个可遍历的对象


1:颠覆常识的yield

function Generator()
{
    for ($i=0; $i<3; $i++)
    {
        echo "输出存在感1\n";
        yield $i;
        echo "输出存在感2\n";
    }
}
echo "#######返回对象#########\n";
var_dump(Generator());//返回对象

echo "\n#######返回对象#########\n";
echo "#######遍历一次情况#########\n";
foreach (Generator() as $key=>$value)
{
    var_dump($value);
    break;
}

echo "\n#######返回对象#########\n";
echo "#######一直遍历情况#########\n";
foreach (Generator() as $key=>$value)
{
    var_dump($value);
}

输出结果:
#######返回对象#########
object(Generator)#1 (0) {
}

#######返回对象#########
#######遍历一次情况#########
输出存在感1
int(0)

#######返回对象#########
#######一直遍历情况#########
输出存在感1
int(0)
输出存在感2
输出存在感1
int(1)
输出存在感2
输出存在感1
int(2)
输出存在感2

进程已结束,退出代码为 0

        1:可能你在这发现了几个东西,和之前php完全不同的认知;

        2:在遍历一次的时候,可以发现调用函数,却没有正常的for循环3次,只循环了一次

        3:在遍历一次的情况时,"存在感2"竟然没有调用,在一直遍历的情况下才调用

再看看另一个例子:

冷暖自知一抹茶ck

冷暖自知一抹茶ck

冷暖自知一抹茶ck

        1:while(true)没有阻塞调用函数下面的代码执行,却导致了下面的echo "额额额"和return 无法执行 

        2:return 返回值竟然是没有作用的 

        3:send(1)时,没有echo "哈哈",send(2)时,才开始出现"哈哈",

2:yield的其他语法

    yield表达式中,也可以赋值,但赋值需要使用括号包裹:

冷暖自知一抹茶ck

    只需要在表达式后面加上$key=>$value,即可生成键值的数据:

冷暖自知一抹茶ck

    在函数前增加引用定义,就可以像returning references from functions(从函数返回一个引用)一样 引用生成值

冷暖自知一抹茶ck

三:特性总结

    1:yield是生成器所需要的关键字,必须在函数内部,有yield的函数叫做"生成器函数"

    2:调用生成器函数时,函数将返回一个继承了Iterator的生成器

    3:yield作为表达式使用时,可将一个值加入到生成器中进行遍历,遍历完会中断下面的语句运行,并且保存状态,当下次遍历时会继续执行(这就是while(true)没有造成阻塞的原因)

    4:当send传入参数时,yield可作为一个变量使用,这个变量等于传入的参数

协程

一:实现个简单的协程

    协程,是一种编程逻辑的转变,使多个任务能交替运行,而不是之前的一直根据流程往下走,举个例子

    当有一个逻辑,每次调用这个文件时,该文件要做3件事:

    1:写入300个文件

    2:发送邮件给500个会员

    3:插入100条数据

/**
 * 任务对象
 * Class Task
 */
class Task {
    protected $taskId;//任务id
    protected $coroutine;//生成器
    protected $sendValue = null;//生成器send值
    protected $beforeFirstYield = true;//迭代指针是否是第一个

    public function __construct($taskId, Generator $coroutine) {
        $this->taskId = $taskId;
        $this->coroutine = $coroutine;
    }

    public function getTaskId() {
        return $this->taskId;
    }

    /**
     * 设置插入数据
     * @param $sendValue
     */
    public function setSendValue($sendValue) {
        $this->sendValue = $sendValue;
    }

    /**
     * send数据进行迭代
     * @return mixed
     */
    public function run() {
        //如果是
        if ($this->beforeFirstYield) {
            $this->beforeFirstYield = false;
            var_dump($this->coroutine->current());
            return $this->coroutine->current();
        } else {
            $retval = $this->coroutine->send($this->sendValue);
            $this->sendValue = null;
            return $retval;
        }
    }

    /**
     * 是否完成
     * @return bool
     */
    public function isFinished() {
        return !$this->coroutine->valid();
    }
}

        这个封装类,可以更好的去调用运行生成器函数,但只有这个也是不够的,我们还需要一个调度任务类,来代替前面的while:

/**
 * 任务调度
 * Class Scheduler
 */
 class Scheduler {
    protected $maxTaskId = 0;//任务id
    protected $taskMap = []; // taskId => task
    protected $taskQueue;//任务队列

    public function __construct() {
        $this->taskQueue = new SplQueue();
    }

    public function newTask(Generator $coroutine) {
        $tid = ++$this->maxTaskId;
        //新增任务
        $task = new Task($tid, $coroutine);
        $this->taskMap[$tid] = $task;
        $this->schedule($task);
        return $tid;
    }

    /**
     * 任务入列
     * @param Task $task
     */
    public function schedule(Task $task) {
        $this->taskQueue->enqueue($task);
    }

    public function run() {
        while (!$this->taskQueue->isEmpty()) {
            //任务出列进行遍历生成器数据
            $task = $this->taskQueue->dequeue();
            $task->run();

            if ($task->isFinished()) {
                //完成则删除该任务
                unset($this->taskMap[$task->getTaskId()]);
            } else {
                //继续入列
                $this->schedule($task);
            }
        }
    }
}

        很好,我们已经有了一个调度类,还有了一个任务类,可以继续实现上面的功能了:

function task1()
{
    for ($i = 0; $i <= 300; $i++) {
        //写入文件,大概要3000微秒
        usleep(3000);
        echo "写入文件{$i}\n";
        yield $i;
    }
}

function task2()
{
    for ($i = 0; $i <= 500; $i++) {
        //发送邮件给500名会员,大概3000微秒
        usleep(3000);
        echo "发送邮件{$i}\n";
        yield $i;
    }
}

function task3()
{
    for ($i = 0; $i <= 100; $i++) {
        //模拟插入100条数据,大概3000微秒
        usleep(3000);
        echo "插入数据{$i}\n";
        yield $i;
    }
}

$scheduler = new Scheduler;

$scheduler->newTask(task1());
$scheduler->newTask(task2());
$scheduler->newTask(task3());

$scheduler->run();

    很好,我们已经实现了可以调度任务,进行任务交叉运行的功能了,

    这就是"协程"协程可以将多个不同的任务交叉运行

二:协程与调度器的通信

        我们在上面已经实现了一个协程封装了,但是任务和调度器缺少了通信,我们可以重新封装下,使协程当中能够获取当前的任务id,新增任务,以及杀死任务

        先封装一下调用的封装:

class YieldCall{
    protected $callback;

    public function __construct(callable $callback)
    {
        $this->callback = $callback;
    }

    /**
     * 调用时将返回结果
     * @param Task $task
     * @param Scheduler $scheduler
     * @return mixed
     */    public function __invoke(Task $task, Scheduler $scheduler)
    {
        $callback = $this->callback;
        return $callback($task, $scheduler);
    }
}

        同时我们需要小小的改动下调度器的run方法:

public function run()
{
    while (!$this->taskQueue->isEmpty()) {
        $task   = $this->taskQueue->dequeue();
        $retval = $task->run();

        //如果返回的是YieldCall实例,则先执行        if ($retval instanceof YieldCall) {
            $retval($task, $this);
            continue;
        }
        if ($task->isFinished()) {
            unset($this->taskMap[$task->getTaskId()]);
        } else {
            $this->schedule($task);
        }
    }
}

        新增 getTaskId函数去返回task_id:

function getTaskId(){
    //返回一个YieldCall的实例    return new YieldCall(
        //该匿名函数会先获取任务id,然后send给生成器,并且由YieldCall将task_id返回给生成器函数
        function (Task $task, Scheduler $scheduler) {
            $task->setSendValue($task->getTaskId());
            $scheduler->schedule($task);
        }
    );
}

        然后,我们再修改下task1,task2,task3函数:

function task1(){
    $task_id = (yield getTaskId());
    for ($i = 0; $i <= 300; $i++) {
        //写入文件,大概要3000微秒        usleep(3000);
        echo "任务{$task_id}写入文件{$i}\n";
        yield $i;
    }
}
function task2(){
    $task_id = (yield getTaskId());
    for ($i = 0; $i <= 500; $i++) {
        //发送邮件给500名会员,大概3000微秒        usleep(3000);
        echo "任务{$task_id}发送邮件{$i}\n";
        yield $i;
    }
}
function task3(){
    $task_id = (yield getTaskId());
    for ($i = 0; $i <= 100; $i++) {
        //模拟插入100条数据,大概3000微秒        usleep(3000);
        echo "任务{$task_id}插入数据{$i}\n";
        yield $i;
    }
}

$scheduler = new Scheduler;

$scheduler->newTask(task1());
$scheduler->newTask(task2());
$scheduler->newTask(task3());

$scheduler->run();

        执行结果:冷暖自知一抹茶ck

        这样的话,当第一次执行的时候,会先调用getTaskId将task_id返回,然后将任务继续执行,这样,我们就获取到了调度器分配给任务的task_id,是不是很神奇?

三:生成新任务以及杀死任务

    现在新增了一个需求:当发送邮件给会员时,需要新增一个发送短信的子任务,当会员id大于200时则停止。

    同时,我们可以利用YieldCall,去新增任务和杀死任务:

/**
 * 传入一个生成器函数用于新增任务给调度器调用
 * @param Generator $coroutine
 * @return YieldCall
 */function newTask(Generator $coroutine) {
    return new YieldCall(
        //该匿名函数,会在调度器中新增一个任务
        function(Task $task, Scheduler $scheduler) use ($coroutine) {
            $task->setSendValue($scheduler->newTask($coroutine));
            $scheduler->schedule($task);
        }
    );
}/**
 * 杀死一个任务
 * @param $tid
 * @return YieldCall
 */function killTask($taskId) {
    return new YieldCall(
        //该匿名函数,传入一个任务id,然后让调度器去杀死该任务
        function(Task $task, Scheduler $scheduler) use ($taskId) {
            $task->setSendValue($scheduler->killTask($taskId));
            $scheduler->schedule($task);
        }
    );
}

        同时,调度器也得有killTask的方法:

/**
 * 杀死一个任务
 * @param $taskId
 * @return bool
 */public function killTask($taskId)
{
    if (!isset($this->taskMap[$taskId])) {
        return false;
    }

    unset($this->taskMap[$taskId]);

    /**
     * 遍历队列,找出id相同的则删除
     */    foreach ($this->taskQueue as $i => $task) {
        if ($task->getTaskId() === $taskId) {
            unset($this->taskQueue[$i]);
            break;
        }
    }

    return true;
}

        有了新增和删除,我们就可以重新写一下task2以及新增task4:

function task4(){
    $task_id = (yield getTaskId());
    while (true) {
        echo "任务{$task_id}发送短信\n";
        yield;
    }
}function task2(){
    $task_id = (yield getTaskId());
    $child_task_id = (yield newTask(task4()));
    for ($i = 0; $i <= 500; $i++) {
        //发送邮件给500名会员,大概3000微秒        usleep(3000);
        echo "任务{$task_id}发送邮件{$i}\n";
        yield $i;
        if($i==200){
           yield killTask($child_task_id);
        }
    }
}

        运行结果:冷暖自知一抹茶ck

        这样我们就完美的实现了新增任务,以及杀死任务了

总结

        前面所说的,协程只是一种编程逻辑,一种写代码的技巧,协程能够帮助我们更好的切换代码中任务 

        从上面的例子不难发现,其实协程实现封装较为麻烦,并且不用协程也能实现这些功能,那为什么要用协程呢? 

        因为协程可以让代码更加的简洁,任务相互之间独立区分开,可以使代码更加的清爽 

        协程让我们可以更好的控制切换任务流 

        前面介绍了那么多,或许有很多人感觉不对,会说"协程不能提升效率吗?","协程到底用来干什么的?" 

        或许由上面的例子很难看出协程的用处,那我们继续举例子吧: 

        js ajax是phper都了解的一个技术, 

        当点击一个按钮时,先将点击事件ajax传输给后端进行增加一条点击数据,然后出现一个动画,这是一个很正常的事,那么请问,如果ajax是同步,并且在网络不好的情况,会发生什么呢? 

        没错,点击之后,页面将会卡几秒(网络不好),请求完毕之后,才会出现一个动画. 

        协程的用处就在这了,我们可以利用协程,把一些同步io等待的代码逻辑,改为异步,在等待的时间内,可以让cpu去处理其他任务, 

        就如同小学时候做的一道题: 

        小明烧开水需要10分钟,刷牙需要3分钟,吃早餐需要5分钟,请问做完这些事情总共需要多少分钟? 

        答案是10分钟,因为在烧开水这个步骤时,不需要坐在那里看水壶烧(异步,io耗时)可以先去刷牙,然后去吃早餐 

        以上就是php yield关于协程的全部内容了

swoole

        由总结可以看出,协程用在最多的应用场景,在于需要io耗时,cpu可以节省出来的场景,并且必须要是异步操作

        这里推荐swoole扩展https://www.swoole.com/  ,

easyswoole

        EasySwoole 是一款基于Swoole Server 开发的常驻内存型的分布式PHP框架,专为API而生,摆脱传统PHP运行模式在进程唤起和文件加载上带来的性能损失。EasySwoole 高度封装了 Swoole Server 而依旧维持 Swoole Server 原有特性,支持同时混合监听HTTP、自定义TCP、UDP协议,让开发者以最低的学习成本和精力编写出多进程,可异步,高可用的应用服务。

        官网:http://www.easyswoole.com/ 


协程有以下特点:

(1)协程的调度由应用程序调度器控制,调度器由开发应用程序者编写。协程在应用层面,进程和线程在操作系统层面。

(2)协作式的调度方式。由自己交出cpu执行权。

(3)减轻了OS处理零散任务和轻量级任务的负担。

(4)消耗更少的资源

你可能已经注意到调用current()之前没有调用rewind().这是因为生成迭代对象的时候已经隐含地执行了rewind操作.

function gen() {
    yield 'foo';
    yield 'bar';
}
 
$gen = gen();
var_dump($gen);
var_dump($gen->send('something'));
 
// 如之前提到的在send之前, 当$gen迭代器被创建的时候一个renwind()方法已经被隐式调用
// 所以实际上发生的应该类似:
//$gen->rewind();
//var_dump($gen->send('something'));
 
//这样renwind的执行将会导致第一个yield被执行, 并且忽略了他的返回值.
//真正当我们调用yield的时候, 我们得到的是第二个yield的值! 导致第一个yield的值被忽略.
//string(3) "bar"

生成器:

1. 迭代器iterator
将数据集合用对象的方式存储起来,使用foreach遍历迭代器实现数据集合的遍历。
2. 生成器与迭代器
生成器generator实现了迭代器,含有迭代器的方法。generator 是 forward-only 的迭代,在迭代开始后不能 rewind。生成器不能被实例
化,也就是直接new。通过含有yield关键字函数返回。在函数里面的yield构成了中断点。
3. yield关键字
可以理解为返回生成器函数的中断点,可以返回数据和向其发送数据。是生成器的关键所在。
4. 生成器与协程
协程的支持是在迭代生成器的基础上, 增加了可以回送数据给生成器的功能(调用者发送数据给被调用的生成器函数). 这就把生成器到调用
者的单向通信转变为两者之间的双向通信.
返回生成器的函数中函数体可以理解为一个协程。生成器调用成员函数控制协程的上下文执行

        PHP7中,通过生成器委托(yield from),可以将其他生成器、可迭代的对象、数组委托给外层生成器。外层的生成器会先顺序 yield 委托出来的值,然后继续 yield 本身中定义的值。

        利用 yield from 可以方便我们编写比较清晰生成器嵌套,而代码嵌套调用是编写复杂系统所必需的。

生成器返回值

        如果生成器被迭代完成,或者运行到 return 关键字,是会给这个生成器返回值的。可以有两种方法获取这个返回值: 

使用 $ret = Generator::getReturn() 方法。
使用 $ret = yield from Generator() 表达式。



进程、线程、协程

进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。

线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是的)。

协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。

PHP中的协程实现基础 yield

yield的根本实现是生成器类,而迭代器类是迭代器接口的实现:



参考:

        PHP7下的协程实现

        PHP中被忽略的性能优化利器:生成器

        php yield关键字以及协程的实现



冷暖自知一抹茶ck
请先登录后发表评论
  • 最新评论
  • 总共0条评论