提出
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
點選 連結,免費加入心智極客的知識星球分享群,共同成長。