PHP 生成器入门

https://juejin.im/entry/5b4c2d76f265da0f697029ad

PHP 在 5.5 版本中引入了「生成器(Generator)」特性,不过这个特性并没有引起人们的注意。在官方的 从 PHP 5.4.x 迁移到 PHP 5.5.x 中介绍说它能以一种简单的方式实现迭代器(Iterator)。

生成器实现通过 yield 关键字完成。生成器提供一种简单的方式实现迭代器,几乎无任何额外开销或需要通过实现迭代器接口的类这种复杂方式实现迭代。

文档提供了一个简单的实例演示这个简单的迭代器,请看下面的代码:

function xrange($start, $limit, $step = 1) {
    for ($i = $start; $i <= $limit; $i += $step) {
        yield $i;
    }
}

让我们将它与无迭代器支持的数组进行比较:

foreach xrange($start, $limit, $step = 1) {
    $elements = [];

    for ($i = $start; $i <= $limit; $i += $step) {
        $elements[] = $i;
    }

    return $elements;
}

这两个版本的函数都支持 foreach 迭代获取所有元素:

foreach (xrange(1, 100) as $i) {
    print $i . PHP_EOL;
}

所以除了一个更短的函数定义,我们还能获取什么呢?yield 到底做了什么?为什么在第一个函数定义时依然可以返回数据,即使没有 return 语句?

先从返回值说起。生成器是 PHP 中的一个很特别的函数。当一个函数包含 yield,那么这个函数即不再是一个普通函数,它永远返回一个「Generator(生成器)」实例。生成器实现了 Iterator 接口,这就是为何它能够进行 foreach 遍历的原因。

接下来我使用 Iterator 接口中的方法,对之前的 foreach 循环进行重写。你可以在 3v4l.org 查看结果。

$generator = xrange(1, 100);

while($generator->valid()) {
    print $generator->current() . PHP_EOL;

    $generator->next();
}

我们可以清楚的看到生成器是更高级的技术,现在让我们编写一个新的生成器示例来更好的理解到底在生成器内部是如何进行处理的吧。

function foobar() {
    print ‘foobar - start‘ . PHP_EOL;

    for ($i = 0; $i < 5; $i++) {
        print ‘foobar - yielding...‘ . PHP_EOL;
        yield $i;
        print ‘foobar - continued...‘ . PHP_EOL;
    }

    print ‘foobar - end‘ . PHP_EOL;
}

$generator = foobar();

print ‘Generator created‘ . PHP_EOL;

while ($generator->valid()) {
    print "Getting current value from the generator..." . PHP_EOL;

    print $generator->current() . PHP_EOL;

    $generator->next();
}
Generator created
foobar - start
foobar - yielding...
Getting current value from the generator...
1
foobar - continued
foobar - yielding...
Getting current value from the generator...
2
foobar - continued
foobar - yielding...
Getting current value from the generator...
3
foobar - continued
foobar - yielding...
Getting current value from the generator...
4
foobar - continued
foobar - yielding...
Getting current value from the generator...
5
foobar - continued
foobar - end

嗯?为什么 Generator created 最先打印出来?这是因为生成器在被使用之前不会执行任何操作。在上例中就是$generator->valid() 这句代码才开始执行生成器。我们看到生成器一直运行到了第一个 yield 时,将控制流程交还给调用者 $generator->valid()。$generator->next() 调用时则恢复生成器执行,到下一个 yield 再次停止运行,如此反复直到没有更多的 yield 为止。我们现在拥有了可以在任何 yield 执行暂停和回复的终端函数。这个特性允许编写客户端所需的延迟函数。

你可以创建一个从 GitHub API 读取所有用户的功能。支持分页处理,但是你可以隐藏这些细节并且仅当需要时再去获取下一页数据。你可以使用 yield 从当前页面获取每个用户数据,直到当前页所有用户获取完成,你就可以再去获取下一页数据。

class GitHubClient {
    function getUsers(): Iterator {
        $uri = ‘/users‘;

        do {
            $response = $this->get($uri);
            foreach ($response->items as $user) {
                yield $user;
            }

            $uri = $response->nextUri;
        } while($uri !== null);
    }
}

客户端可以迭代出所有用户或者在任何时候停止遍历。

把生成器当迭代器使用真是无聊

是的,你的想法是对的。以上我给出的所有讲解任何人都可以从 PHP 文档中获取到。但是作为迭代器这些使用,连它强大功能的一半都没用到。生成器还提供了不属于 Iterator 接口的 send() 和 throw() 功能。我们前面谈到了暂停和恢复生成器执行功能。当需要恢复生成器时,不仅可以功过 Generator::next() 方法,还可以使用 Generator::send() 和 Generator::throw()方法。

Generator::send() 允许你指定 yield 的返回值,而 Generator::throw() 允许向 yield 抛出异常。通过这些方法我们不仅可以从生成器中获取数据,还能向生成器中发送新数据。

让我们看一个从 Cooperative multitasking using coroutines(强烈推荐阅读本文)摘取的 Logger 日志示例。

function logger($filename) {
    $fileHandle = fopen($filename, ‘a‘);

    while (true) {
        fwrite($fileHandle, yield . "\n");
    }
}

$logger = logger(__DIR__ . ‘/log‘);
$logger->send(‘Foo‘);
$logger->send(‘Bar‘);

yield 在这里是作为表达式使用的。当我们发送数据时,从 yield 返回数据然后作为参数传入到 fwrite()。

讲真,这个示例在实际项目中没毛用。它仅仅用于演示 Generator::send() 的使用原理,但是仅仅能够发送数据并没有太大作用。如果有一个类和普通函数支持的话就不一样了。

使用生成器的乐趣来自于通过 yield 创建数据,然后由「生成器执行程序(generator runner)」依据这个数据来处理业务,然后再继续执行生成器。这就是「协程(coroutines)」和「状态流解析器(stateful streaming parsers)」实例。在讲解协程和状态流解析器之前,我们快速浏览一下如何在生成器中返回数据,我们还没有将接触这方面的知识。从 PHP 5.5 开始我们可以在生成器内部使用 return; 语句,但是不能返回任何值。执行 return; 语句的唯一目的是结束生成器执行。

不过从 PHP 7.0 起支持返回值。这个功能在用于迭代时可能有些奇怪,但是在其他使用场景如协程时将非常有用,例如,当我们在执行一个生成器时我们可以依据返回值处理,而无需直接对生成器进行操作。下一节我们将讲解 return 语句在协程中的使用。

异步生成器

Amp 是一款 PHP 异步编程的框架。支持异步协程功能,本质上是等待处理结果的占位符。「生成器执行程序」为 Coroutine类。它会订阅异步生成器(yielded promise),当有执行结果可用时则继续生成器处理。如果处理失败,则会抛出异常给生成器。你可以到 amphp/amp 版本库查看实现细节。在 Amp 中的 Coroutine 本身就是一个 Promise。如果这个协程抛出未经捕获的异常,这个协程就执行失败了。如果解析成功,那么就返回一个值。这个值看起来和普通函数的返回值并无二致,只不过它处于异步执行环境中。这就是需要生成器需要有返回值的意义,这也是为何我们将这个特性加入到 PHP 7.0 中的原因,我们会将最后执行的yield 值作为返回值,但这不是一个好的解决方案。

Amp 可以像编写阻塞代码一样编写非阻塞代码,同时允许在同一进程中执行其它非阻塞事件。一个使用场景是,同时对一个或多个第三方 API 并行的创建多个 HTTP 请求,但不限于此。得益于事件循环,可以同时处理多个 I/O 处理,而不仅仅是只能处理多个 HTTP请求这类操作。

Loop::run(function() {
    $uris = [
        "https://google.com/",
        "https://github.com/",
        "https://stackoverflow.com/",
    ];

    $client = new Amp\Artax\DefaultClient;
    $promises = [];

    foreach ($uris as $uri) {
        $promises[$uri] = $client->request($uri);
    }

    $responses = yield $promises;

    foreach ($responses as $uri => $response) {
        print $uri . " - " . $response->getStatus() . PHP_EOL;
    }
});

但是,拥有异步功能的协程并非只能够在 yield 右侧出现变量,还可以在它的左侧。这就是我们前面提到的解析器。

$parse = new Parser((function(){
    while (true) {
        $line = yield "\r\n";

        if (trim($line) === "") {
            continue;
        }

        print "New item: {$line}" . PHP_EOL;
    }
})());

for ($i = 0; $i < 100; $i++) {
    $parser->push("bar\r");
    $parser->push("\nfoo");
}

解析器会缓存所有输入直到接收的是 \r\n。这类生成器解析器并不能简化简单协议处理(如换行分隔符协议),但是对于复杂的解析器,如在服务器解析 HTTP 请求的 Aerys

小结

生成器的功能远超多数人的认知范围。对于一些朋友来说可能是首次接触生成器相关知识,一些朋友可能已经将它作为迭代器来使用,仅有很少一部分朋友使用生成器处理更多的事情。获取你有一些很赞的想法?我很乐意进一步探讨这些项目,并且希望你能从中学习到一些知识。:)

如果你需要更多资料,我推荐你阅读 nikic 写的 使用生成器处理多任务

原文

An Introduction to Generators in PHP

原文地址:https://www.cnblogs.com/jiangxiaobo/p/10175286.html

时间: 2024-10-08 09:52:37

PHP 生成器入门的相关文章

迭代器和生成器入门

Python 迭代器生成器 迭代器.生成器这些概念名称真是让人头大,其实它们的原理特别简单.深刻. 可迭代对象(iterable) 在讲迭代器和生成器之前,必须要讲的一个概念就是可迭代对象. 可迭代对象之前需要聊一下Python中的那些内置数据结构--列表.字典.集合.元组等,这些数据结构就像一个装有内置数据的容器. 这里可以这么想--把数据想象成苹果,把列表.字典.集合.元组等想像成装苹果的袋子.盒子.篮子.筐子等装苹果的容器. 我们都能从这些容器中一个一个把所有苹果拿出来,这就像是我们经常使

php 生成器 入门理解

概念太晦涩,看不懂,直接上例子: 问题:得到一个1-1000000的整数数组,然后用foreach遍历输出 如果没有生成器,这样做: $arr=range(1,1000000);//这个函数最终会返回一个数组; foreach ($arr as $key => $value) {//此时遍历的是整个放在内存中的数组 echo $key.'=>'.$value.'<br />'; //输出}//总结:由迭代器我们可以知道,foreach的时候,每次遍历都会操作内存中的数组的键值,改变

Hibernate入门(二)之hibernate的内部执行过程,主键生成器,对象的状态

内部执行过程 主键生成器 Identity(常用) 1.表必须支持自动增长机制 2.数据库生成主键 3.不需要在程序中设置主键 Assigned 必须通过程序的方式给值才可以 person.setId(xxx): 一般用于开发的时候测试使用 Increment(常用) 1.如果选择该主键的生成方式,则必须是数字类型 2.先获取主键的最大值,在最大值的基础上加1,形成新的主键 3.效率比较低,因为这种方式会先select表中最大的主键值 4.主键的生成是由hibernate内部实现的 native

Net作业调度(一) -Quartz.Net入门 Quartz表达式生成器 [转]

背景 很多时候,项目需要在不同个时刻,执行一个或很多个不同的作业. Windows执行计划这时并不能很好的满足需求了. 这时候需要一个更为强大,方便管理,集部署的作业调度了. 介绍 Quartz一个开源的作业调度框架,OpenSymphony的开源项目.Quartz.Net 是Quartz的C#移植版本. 它一些很好的特性: 1:支持集群,作业分组,作业远程管理. 2:自定义精细的时间触发器,使用简单,作业和触发分离. 3:数据库支持,可以寄宿Windows服务,WebSite,winform等

Python入门篇(八)之迭代器和生成器

迭代器和生成器 1.列表生成式 列表生成式即List Comprehensions,是Python内置的非常简单却强大的可以用来创建list的生成式.举个例子,要生成list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]可以用list(range(1, 11)): >>> list(range(1, 11)) [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 但如果要生成[1x1, 2x2, 3x3, ..., 10x10]怎么做?方法一是循环: >

迭代器和生成器(python3入门)

#可迭代的对象:但凡数据类型可以执行__iter__() # ''.__iter__() # [].__iter__() # (1,2).__iter__() # {'x':1}.__iter__() # {1,2,3}.__iter__() # open('a.txt').__iter__() #调用可迭代对象的__iter__()方法,会得到一个返回值,该返回值就是迭代器对象 #迭代器对象:既内置有__iter__()方法,又内置又__next__()方法.例如:文件本身就是迭代器对象 #迭

Python入门-生成器和生成器表达式

昨天我们说了迭代器,那么和今天说的生成器是什么关系呢? 一.生成器 什么是生成器?说白了生成器的本质就是迭代器. 在Python中中有三种方式来获取生成器. 1.通过生成器函数 2.通过各种推导式来实现生成器 3.通过数据的转换也可以获取生成器 首先,让我们看一个很简单的函数: def func(): print(111) return 222 ret = func() print(ret) 结果: 111 222 将函数中的return换成yield就是生成器 def func(): prin

Python 入门基础11 --函数基础4 迭代器、生成器、枚举类型

今日目录: 1.迭代器 2.可迭代对象 3.迭代器对象 4.for循环迭代器 5.生成器 6.枚举对象 一.迭代器: 循环反馈的容器(集合类型) 每次重复即一次迭代,并且每次迭代的结果都是下一次迭代的初始值 l = [1, 2, 3] count = 0 while count<len(l): print(l[count]) count += 1 1.1 为什么要有迭代器? 字符串.列表.元组可以通过索引的方式迭代取出其中包含的元素 字典.集合.文件等类型,没有索引,只有通过不依赖索引的迭代方式

(一)Python入门-4控制语句:10推导式创建序列-列表推导式-字典推导式-集合推导式-生成器推导式

推导式创建序列: 推导式是从一个或者多个迭代器快速创建序列的一种方法.它可以将循环和条件判断结合, 从而避免冗长的代码.推导式是典型的Python 风格,会使用它代表你已经超过Python初 学者的水平. 一:列表推导式 列表推导式生成列表对象,语法如下: [表达式 for item in 可迭代对象 ] 或者:{表达式 for item in 可迭代对象 if 条件判断} 1 #列表推导式 2 x = [x for x in range(1,5)] 3 print(x) 4 5 x = [x*