PHP 核心特性 - 生成器基础篇
提出
PHP RFC 里描述了生成器的提出过程。考虑这样的需求
实现一个函数,用于获取文件内容,并可对文件内容进行遍历。
实现 1 - 普通函数
最普通的方式就是一次性读取文件内容,然后再进行遍历。
<?php
function getLinesFromFile($fileName) {
// 打开文件
if (!$fileHandle = fopen($fileName, 'r')) {
return;
}
// 一次读取每一行并保存
$lines = [];
while (false !== $line = fgets($fileHandle)) {
$lines[] = $line;
}
fclose($fileHandle);
return $lines;
}
$lines = getLinesFromFile('test.txt');
foreach ($lines as $line) {
}
当使用该函数读取大文件时,就会因为内存不足而报错。
PHP Fatal error: Allowed memory size of 134217728 bytes exhausted
实现 2 自定义对象
既然一次性读取文件内容行不通,只能考虑边读取边遍历,可使用面向对象的思想来解决。
PHP 中的对象,只要实现了 Iterator
接口,就可用 foreach
来进行遍历。
Iterator extends Traversable {
// 返回当前索引游标指向的元素
abstract public current ( void ) : mixed
// 返回当前索引游标指向的键
abstract public key ( void ) : scalar
// 移动当前索引游标到下一元素
abstract public next ( void ) : void
// 重置索引游标
abstract public rewind ( void ) : void
// 判断当前索引游标指向的元素是否有效
abstract public valid ( void ) : bool
}
利用这点,可以手动实现 Iterator
接口,来实现边读取文件边进行遍历功能。
<?php
class LineIterator implements Iterator {
protected $fileHandle;
protected $line;
protected $i;
public function __construct($fileName) {
if (!$this->fileHandle = fopen($fileName, 'r')) {
throw new RuntimeException('Couldn\'t open file "' . $fileName . '"');
}
}
public function rewind() {
fseek($this->fileHandle, 0);
$this->line = fgets($this->fileHandle);
$this->i = 0;
}
public function valid() {
return false !== $this->line;
}
public function current() {
return $this->line;
}
public function key() {
return $this->i;
}
public function next() {
if (false !== $this->line) {
$this->line = fgets($this->fileHandle);
$this->i++;
}
}
public function __destruct() {
fclose($this->fileHandle);
}
}
$lines = new LineIterator('test.txt');
foreach ($lines as $line) {
echo $line;
}
实现 3 - 生成器
该问题很典型,在很多情景中都会出现一次性读取的内存不足问题。为了避免每一次都要手动实现 Iterator
接口,PHP 提供了生成器来解决该问题。也就是说,生成器已经帮我们实现了 Iterator
接口,因此可以直接使用。
<?php
function getLinesFromFile($fileName) {
if (!$fileHandle = fopen($fileName, 'r')) {
return;
}
while (false !== $line = fgets($fileHandle)) {
yield $line;
}
fclose($fileHandle);
}
$lines = getLinesFromFile('test.txt');
foreach ($lines as $line) {
}
使用生成器,既保持了代码的简洁,也降低了性能开销,效率也比自己定义类要高。
本质
基本语法
生成器使用 yield
关键字来定义,主要有三种定义方法,分别生成 null
、值以及键值对
yield;
yield $value;
yield $key => $value;
Generator 对象
生成器看上去是函数,实际上是 Generator
类的实例。
function simpleGenerator()
{
yield;
}
echo get_class(simpleGenerator()) // Generator
既然是对象,就可以将其赋值给变量。
$gen = simpleGenerator();
Generator
对象已经实现了 Iterator
接口
$gen instanceof Iterator // true
内部结构
我们来看一下 Generator
的内部结构。
final class Generator implements Iterator {
// 实现 Iterator 接口
void rewind();
bool valid();
mixed current();
mixed key();
void next();
// 传入值
mixed send(mixed $value);
// 传入异常
mixed throw(Exception $exception);
// 防止被序列化
public __wakeup ( void ) : void
}
函数说明
__wakeup
- 抛出异常以表示生成器不能被序列化
serialize($gen); // Exception with message 'Serialization of 'Generator' is not allowed'
rewind
、valid
、current
、key
、next
方法是对迭代器接口的实现。首先定义生成器
function myGenerator()
{
echo "第一次开始\n";
yield;
echo "第二次开始\n";
yield "值2";
echo "第三次开始\n";
yield "键" => "值3";
echo "结束";
}
$gen = myGenerator();
触发生成器 - 无论执行哪个方法(rewind
, valid
, current
, key
, next
、send
)都会触发生成器,然后先执行 yield
之前的语句。
$gen->valid();
// 先触发生成器,执行代码,所以会打印 "第一次开始"
// 执行 valid() 方法,当生成器关闭时候将返回 false。所以本次返回 true。
current
方法将返回传递给生成器的值或者返回一个 null
$gen->current(); // null
next
方法重新开始下一个生成器
$gen->next(); // 第二次开始
rewind
方法在生成器里面是没有意义的,也就是说,生成器不能像数组那样重新设置索引,只能继续执行或者停止。所以如果是开始的时候执行该方法将会返回 null
,而当第一个 yield
执行后,使用 rewind
方法,就会抛出异常。
$gen->rewind(); // Exception with message 'Cannot rewind a generator that was already run'
第二次 yield
开始后,与之前的流程类似
$gen->current(); // "值2"
$gen->next(); // 第三次开始
$gen->key(); // "键"
$gen->current(); // "值3"
$gen->next(); // 结束
$gen->valid(); // false
这几个函数实现了迭代接口,因此,可以用 foreach
来对生成器进行迭代
<?php
function genRange($start, $limit){
for ($i = $start; $i <= $limit; $i++) {
yield $i;
}
}
$gen = genRange(1, 10);
foreach ($gen as $value) {
echo $value." ";
}
foreach
语句等价于
while ( $gen->valid()) {
echo $gen->current()." ";
$gen->next();
}
输出结果
1 2 3 4 5 6 7 8 9 10
throw
- 向生成器传入异常
function gen() {
try {
yield;
} catch (Exception $e) {
echo "Exception: {$e->getMessage()}\n";
}
echo "Bar\n";
}
$gen = gen();
$gen->throw(new Exception('Test')); // echos "Exception: Test" and "Bar"
send
- 向生产器发送值,该值将会替换当前表达式上下文的结果,并进行下一次迭代(相当于替换值然后执行 next()
)
function echoLogger() {
while (true) {
// 接受外部的传值
$log = yield;
echo 'Log: ' . $log . "\n";
}
}
$logger = echoLogger();
$logger->send('Foo'); // Log: foo
$logger->send('Bar'); // Log: bar
本作品采用《CC 协议》,转载必须注明作者和本文链接
:+1: