Generators是PHP5.5才加入的全新功能,原本有點猶豫是否要嘗試一下,因為Javascript1.7就加入了這個新特性,雖然用法稍有不同(本來其實一樣,但是剛剛看了一下MDN,發現他把文件都改了,舊的用法舊屍骨無存),但是是大同小異的。不過看到文件中介紹SPL中的Iterator與Generator的比較,感覺Generator真的很好用,所以還是拿來作Iterator的練習,然後跟Generator來比較看看。
在試用之前,需要先安裝好PHP5.5...如果使用Mac或是Linux,強烈建議使用C9S大神的神作:phpbrew,使用他可以協助安裝/編譯不同版本的PHP,然後用指令切換...不支援Windows
參考:
* PHP: Generators - Manual
* PHP: Generator - Manual (PHP內建的Interface,從PHP5.5.0開始支援)
* PHP: Iterator - Manual (PHP內建的Interface,從PHP5.0.0開始支援)
基本上只要實作了Iterator的物件,除了透過介面存取,也可以透過foreach來操作。一個最簡單的例子:
<?php
class a implements Iterator {
var $keys = array("k1", "k2", "k3");
var $vals = array("v1", "v2", "v3");
var $pos = 0;
function current() {
return $this->vals[$this->pos];
}
function key() {
return $this->keys[$this->pos];
}
function next() {
$this->pos++;
}
function rewind() {
$this->pos = 0;
}
function valid() {
if($this->pos>=count($this->keys)) {
return false;
}
else {
return true;
}
}
}
$a = new a;
foreach($a as $k=>$v) echo "$k:::$v\n";
執行結果:
Feng-Hsu-Pingteki-MacBook-Air:ironman6 fillano$ php 1-4a.php
k1:::v1
k2:::v2
k3:::v3
利用Iterator,就可以把許多複雜的循序操作,包裝成可以透過foreach來迭代的物件。例如需要剖析一個很大的csv檔,如果一次把檔案讀入記憶體,會使用掉很大的資源,比較好的處理方式是用streaming的方式讀取,逐步處理讀取的內容。這個動作如果包裝成Iterator,就可以透過foreach來築行處理csv。例如:
<?php
class a implements Iterator {
private $count = 0;
private $fd;
private $buffer='';
function __construct($file) {
$this->fd = fopen($file, 'r+');
}
function __destruct() {
fclose($this->fd);
}
function current() {
return explode(',', $this->buffer);
}
function key() {
return $this->count;
}
function next() {
$this->count++;
$this->buffer = fgets($this->fd);
}
function rewind() {
$this->count = 0;
fseek($this->fd, 0);
$this->buffer = fgets($this->fd);
}
function valid() {
if($this->buffer) {
return true;
}
else {
return false;
}
}
}
$a = new a('1-4b.csv');
foreach($a as $line) echo implode(' : ', $line);
echo "\n";
讀取的csv檔案內容如下:
a,b,c
d,e,f
h,i,j
k,l,m
n,o,p
程式執行的結果:
Feng-Hsu-Pingteki-MacBook-Air:ironman6 fillano$ php 1-4b.php
a : b : c
d : e : f
h : i : j
k : l : m
n : o : p
大致上介紹了一下Iterator,可以看到使用他可以把一個循序操作的過程包裝起來,然後利用foreach把它當作陣列來操作。(除了檔案,我想資料庫查詢的過程也可以用這個方法包裝成recordset)不過為了要能這樣操作,就需要打破程式的流程,把步驟分散到各個不同方法裡,其實是有一點麻煩的。使用Generator的話,就可以用更簡單的方式來做,只要在適當的時機使用yield。
如果把上面的例子改成用Generator來做,就變成這樣:
<?php
function getcsv($file) {
$fd = fopen($file, 'r+');
if($fd) {
while(($buffer=fgets($fd))!==false) {
yield explode(',',$buffer);
}
fclose($fd);
}
}
$a = getcsv('1-4b.csv');
foreach($a as $line) echo implode(' : ', $line);
echo "\n";
執行的結果是一樣的:
Feng-Hsu-Pingteki-MacBook-Air:ironman6 fillano$ phpbrew switch php-5.5.4
Feng-Hsu-Pingteki-MacBook-Air:ironman6 fillano$ php 1-4c.php
a : b : c
d : e : f
h : i : j
k : l : m
n : o : p
使用了yield的函數,執行後會回傳一個Generator,而Generator也implement了Iterator,所以一樣可以用foreach來操作,只是寫起來更簡單。再來看一下更複雜的Generator。
除了Iterator的介面,Generator的介面定義了一個方法,叫做send。透過send方法,可以把資料回傳給yield。像這樣:
<?php
function gen() {
$arr = array('a,b,c', 'd,e,f', 'g,h,i', 'j,k,l', 'm,n,o');
foreach($arr as $l) {
$r = (yield $l);
echo $r;
}
}
$a = gen();
while($a->valid()) {
$a->send(implode(' : ', explode(',',$a->current()))."\n");
$a->next();
}
執行結果:
Feng-Hsu-Pingteki-MacBook-Air:ironman6 fillano$ php 1-4d.php
a : b : c
g : h : i
m : n : o
接收yield回傳值,需要用括號把yield包起來,這跟Javascript的yield稍微不一樣。如果是要做一些檔案資料格式轉換,用Generator來做就很方便,而且因為轉換的動作是在Generator外部,所以寫一個Generator,就可以做不同格式的轉換,在Generator函數內存檔,一些複雜的動作就一次做完了。把上面的例子改一下,然後把csv轉成tab seperated txt看看:
<?php
function conv($from, $to) {
$fd1 = fopen($from, 'r+');
$fd2 = fopen($to, 'w+');
if($fd1 && $fd2) {
while(($r=fgets($fd1))!==false) {
$w = (yield $r);
fputs($fd2, $w);
}
fclose($fd1);
fclose($fd2);
}
}
$a = conv('1-4b.csv', '1-4e.txt');
while($a->valid()) {
$a->send(implode("\t", explode(',',$a->current())));
$a->next();
}
執行結果:
Feng-Hsu-Pingteki-MacBook-Air:ironman6 fillano$ php 1-4e.php
Feng-Hsu-Pingteki-MacBook-Air:ironman6 fillano$ cat 1-4e.txt
a b c
h i j
n o p
如果要做其他方式的轉換,就再呼叫一次conv(),然後迭代、轉換、回傳就可以,非常方便又有彈性。
本來覺得Generator沒什麼特別,沒想到試了一下還是很有趣呢...先就此打住,睡個覺以後還要去phpconf...