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'

rewindvalidcurrentkeynext 方法是对迭代器接口的实现。首先定义生成器

function myGenerator()
{   

    echo "第一次开始\n";
    yield;

    echo "第二次开始\n";
    yield "值2";

    echo "第三次开始\n";

    yield "键" => "值3";
    echo "结束";
}

$gen = myGenerator();

触发生成器 - 无论执行哪个方法(rewind, valid, current, key, nextsend)都会触发生成器,然后先执行 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 协议》,转载必须注明作者和本文链接
本帖由系统于 4年前 自动加精
讨论数量: 1

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!