今更感ありますが、PHP 5.5 の目玉機能である finally、ジェネレータ、コルーチンを使ってみました。
finally
これまでの PHP の例外処理だと fclose などの後処理を確実に行うためには try ブロックの最後と catch ブロックの両方に書く必要がありました。
デストラクタでも後処理を書けますが・・・↓のような理由で、ちょっと・・・と思っていたので finally が出来てよかったです。
- PHP 5.4 は クロージャーに $this が暗黙的に束縛されるため 5.3 と動作が異なってしまうことがある
- PHPでインスタンスを作成するメソッドをチェーンした場合のオブジェクトの寿命
- オブジェクトが例外の呼び出し履歴に参照されてデストラクタが呼ばれない
- カスタムエラーハンドラから例外を投げるとスコープ内の変数が例外から参照される
finally を使わない場合は次のように try と catch で同じ処理が必要です。
<?php function hoge() { $fp = fopen(__FILE__ . '.log', 'w'); try { // なにかの処理 throw new Exception("error!"); // 正常時の後処理 fclose($fp); } catch (Exception $ex) { // 例外時の後処理 fclose($fp); throw $ex; } }
finally が実装されたことで後処理を finally だけに書くことが出来るようになりました。
<?php function hoge() { $fp = fopen(__FILE__ . '.log', 'w'); try { // なにかの処理 throw new Exception("error!"); } catch (Exception $ex) { echo "catch {$ex->getMessage()}\n"; throw $ex; } finally { // 後処理 echo "finally\n"; fclose($fp); } }
実行結果
catched error!
finally
finally の用途は例外処理だけではありません。次のように try ブロックの途中で return した場合でも finally ブロックは実行されます。
<?php function hoge() { try { echo "begin\n"; return; echo "end1\n"; } finally { // return で抜けても finally は実行される echo "finally\n"; } echo "end2\n"; }
実行結果
begin
finally
return で関数を抜けているので end1 や end2 は実行されませんが、finally は実行されます。finally ブロックに後処理を書けばどのような方法で関数を抜けたとしても後処理を確実に実行することが出来ます。
ただし exit や fatal error で終了した場合は別です。次の例では try ブロックで exit で終了しているため finally は実行されません。
<?php function hoge() { try { echo "begin\n"; exit(0); } finally { // exit で終了した場合は実行されない echo "finally\n"; } }
実行結果
begin
ジェネレータ
今まで私が触ってきた言語には無かったものです! setjmp? longjmp? そんな恐ろしいものは知りません(>_<)
実際の例が判りやすいです。次のように関数の中で yield キーワードを使うとその関数の戻り値がジェネレータになります(マニュアルのまんまコピペ)。
<?php function xrange($start, $limit, $step = 1) { for ($i = $start; $i <= $limit; $i += $step) { yield $i; } } foreach (xrange(1, 9, 2) as $number) { echo "$number "; }
実行結果
1 3 5 7 9
実行結果だけ見ると普通の range と全く同じです。
foreach (range(1, 9, 2) as $number) { echo "$number "; }
実行結果
1 3 5 7 9
が、処理の内容は全く異なります。ただの range は array(1, 3, 5, 7, 9)
という配列を戻り値として返しているだけですが、xrange は yield に来たところで処理が一旦中断され、呼び出し元の foreach ブロックに処理が戻り、次のループで xrange の処理が再開されています。
次のコードで出力結果をよく見てみると、hoge 関数の yield と foreach ブロックの中身が交互に実行されていることが判ります。
<?php function hoge() { echo "+ 1\n"; yield 1; echo "+ 2\n"; yield 2; echo "+ 3\n"; yield 3; echo "+ end\n"; } foreach (hoge() as $i) { echo "- $i\n"; }
実行結果
+ 1
- 1
+ 2
- 2
+ 3
- 3
+ end
連想配列を返すことも出来ます。
<?php function hoge() { yield "a" => 1; yield "b" => 2; yield "c" => 3; } foreach (hoge() as $k => $v) { echo "$k => $v\n"; }
実行結果
a => 1
b => 2
c => 3
ジェネレータの実体は Generator というクラスです。リフレクションで概要が見れます。
<?php function hoge() { yield 1; yield 2; yield 3; } ReflectionClass::export(hoge());
実行結果
Class [ <internal:Core> <iterateable> final class Generator implements Iterator, Traversable ] {
- Constants [0] {
}
- Static properties [0] {
}
- Static methods [0] {
}
- Properties [0] {
}
- Methods [7] {
Method [ <internal:Core, prototype Iterator> public method rewind ] {
- Parameters [0] {
}
}
Method [ <internal:Core, prototype Iterator> public method valid ] {
- Parameters [0] {
}
}
Method [ <internal:Core, prototype Iterator> public method current ] {
- Parameters [0] {
}
}
Method [ <internal:Core, prototype Iterator> public method key ] {
- Parameters [0] {
}
}
Method [ <internal:Core, prototype Iterator> public method next ] {
- Parameters [0] {
}
}
Method [ <internal:Core> public method send ] {
- Parameters [1] {
Parameter #0 [ <required> $value ]
}
}
Method [ <internal:Core> public method __wakeup ] {
- Parameters [0] {
}
}
}
}
foreach できるので当たり前ですが Iterator や Traversable を実装しています。なので次のように IteratorAggregate で簡単にイテレータを実装出来ます。
<?php class Hoge implements IteratorAggregate { private $_list = array(1, 2, 3); public function getIterator() { foreach ($this->_list as $i) { yield $i; } } } $hoge = new Hoge(); foreach ($hoge as $i) { echo "$i "; }
実行結果
1 2 3
注意しなければならない点としては foreach を途中で break するとその後の処理は呼ばれないということです。
<?php function hoge() { echo "begin\n"; yield 1; yield 2; yield 3; // 呼び出し元で break しているので呼ばれない echo "end\n"; } foreach (hoge() as $i) { echo "$i\n"; break; }
実行結果
begin
1
もしジェネレータで後処理が必要な場合は finally を使うことが出来ます。
<?php function hoge() { echo "begin\n"; try { yield 1; yield 2; yield 3; } finally { echo "end\n"; } } foreach (hoge() as $i) { echo "$i\n"; break; }
実行結果
begin
1
end
少し実用的な例として、次のようなものが考えられるでしょうか。ジェネレータを複数繋げてパイプラインっぽく処理しています。ファイル全体を読み込んで array 系関数で処理するよりもメモリ使用量の面でお得です。
<?php class g { public static function readline($fn) { $fp = fopen($fn, "r"); try { while (!feof($fp)) { yield fgets($fp); } } finally { fclose($fp); } } public static function trim($g) { foreach ($g as $val) { yield trim($val); } } public static function toupper($g) { foreach ($g as $val) { yield strtoupper($val); } } } foreach (g::toupper(g::trim(g::readline(__FILE__))) as $line) { echo "$line\n"; }
実行結果
<?PHP
CLASS G
{
~中略~
FOREACH (G::TOUPPER(G::TRIM(G::READLINE(__FILE__))) AS $LINE)
{
ECHO "$LINE\N";
}
どこかの英語サイトで見た 協調的マルチタスク の簡易版です。taskA と taskB が並列に実行されます(引用元は忘れました! すみません(>_<))。
<?php function taskA() { yield; for ($i=1; $i<=10; $i++) { echo "taskA $i\n"; yield; } } function taskB() { yield; for ($i=1; $i<=5; $i++) { echo "taskB $i\n"; yield; } } $tasks = [taskA(), taskB()]; reset($tasks); while (count($tasks)) { if (list ($i, $task) = each($tasks)) { if ($task->valid()) { $task->send(null); } else { unset($tasks[$i]); } } else { reset($tasks); } }
実行結果
taskA 1
taskB 1
taskA 2
taskB 2
taskA 3
taskB 3
taskA 4
taskB 4
taskA 5
taskB 5
taskA 6
taskA 7
taskA 8
taskA 9
taskA 10
上手く使えばソケットの非ブロッキングI/Oとかがやりやすくなりそうです。
コルーチン
コルーチンは実体としてはジェネレータと同じものです、ジェネレータの send メソッドを使います。
send は1つの引数を取ります。yield で停止したジェネレータの処理を継続させるとともに send に指定した引数が yield の戻り値となります。
<?php function hoge() { for (;;) { var_dump(yield); } } $hoge = hoge(); $hoge->send("a"); $hoge->send("b"); $hoge->send("c");
実行結果
string(1) "a"
string(1) "b"
string(1) "c"
実用的かどうかわかりませんが、次のようなコードを思いつきました。
ジェネレータ(コルーチン)を使わない場合
<?php class Logger { private $_fp; public function __construct($fn) { $this->_fp = fopen($fn, "a"); } public function __destruct() { fclose($this->_fp); } public function put($log) { fwrite($this->_fp, $log . PHP_EOL); } } $logger = new Logger(__FILE__.'.log'); $logger->put("aaa"); $logger->put("bbb");
ジェネレータ(コルーチン)を使った場合
<?php function logger($fn) { $fp = fopen($fn, 'a'); try { for (;;) { fwrite($fp, yield . PHP_EOL); } } finally { fclose($fp); } } $logger = logger(__FILE__.'.log'); $logger->send('aaa'); $logger->send('bbb');