それでもPHPにfinallyが必要な理由

Posted by Hiraku on 2012-09-28

PHP Conference 2012で知ったのですが、PHP5.5にはfinallyが搭載される見込みだそうです。

搭載されるのはいいのですが、昔、「PHPにfinallyはないけどデストラクタがあるよ」と題してfinally不要説を書いたことがあるので、もう少し考察を行ってみたいと思います。自分で自分に反論を書いてるのもアレなんですけど。

※RFCは追いかけてなかったので、本記事は想像で書いています。ツッコミください。

finally不要説

私が主張した内容を要約するとこんな感じです。

  1. finallyの主な用途はファイルのクローズやDBの接続断などの「後始末処理」である。
  2. 後始末はデストラクタで行うこともできる。
  3. PHPではデストラクタの動作が保障されている。(参照カウントによるGC)
  4. ゆえにデストラクタを正しく使えばfinallyは不要。

具体例を出すと、finallyのRFCでは例としてmysqli関数の呼び出しが書かれています。

$db = mysqli_connect();
try {
   call_some_function($db);//the function may throw exceptions which we can not handle
} finally {
   mysqli_close($db);
}

デストラクタをきちんと使っているなら、こう書けるはずです。クローズはデストラクタが責任もって行うのだから、わざわざ使う側が明示的にクローズする必要はないのです。

$db = new mysqli();
call_some_function($db);

//(変数$dbが消滅した時点で接続解放が自動で行われる)

つまり、finallyが必要だってわめいてる連中はプログラムの書き方が下手糞なんだよ!!やーいやーいm9(^Д^)

…さて、自分で書いておいてなんですが、この論には一点、間違いがあります。3番目の「PHPではデストラクタの動作が保障されている」という部分が正確ではありません。「動作保証はされている。ただし動作のタイミングは状況による」といった方が正確です。循環参照時はオブジェクトの解放タイミングがずれるんですよね。

循環参照

循環参照とは、参照をたどっていくと元に戻ってくるような状態です。$aのプロパティで$a自身を持ってしまっているような場合がこれです。

$a = new stdClass;
$a->self = $a;

実際はこんなに単純なものはあまりなくて、もっとたくさんのオブジェクトが複雑に絡み合って、まわりまわって循環参照になっていることもあります。(そしてそういうケースの方が多いでしょう)

循環参照になってしまうと、たとえ$aという変数がなくなっても$a自身がつかんだままのため参照数がゼロにならず、参照カウンタが有効に働かなくなってしまいます。オブジェクトが解放されず、デストラクタも起動せず、残り続けてしまいます。

ただ、PHP5.3から搭載された循環参照コレクタがあるので、ある程度メモリがひっ迫したらこのようなゾンビオブジェクトも解放されるようになっています。しかし思ったタイミングでデストラクタが起動しないという問題があるのです。

ではどうすればいいのでしょうか。方針としては循環参照を避けるか、循環参照ができても問題なく動作するようにするのどちらかしかありません。

循環参照を避けるWeakref

まず、循環参照を避ける方式から考えてみます。

循環参照を作らないように注意深くプログラミングすればいい!と言うのは簡単ですが、少し複雑なプログラムを書くと、意外とすぐに循環参照が紛れ込みますし、あえて循環参照を作った方がわかりやすく書けるケースも多いでしょう。

循環参照を作りつつ、しかし参照カウンタの挙動を妨げないようにするには、pecl/Weakrefモジュールを使う方法があります。PHPの通常の挙動とは異なり、参照カウンタを増やさない参照(弱参照)を作ることができます。

あらかじめ循環参照になりそうな部分をWeakrefにして参照を切っておけば、参照カウンタは期待通りに動いてくれます。RAIIでオブジェクト解放を行えばよく、finallyも必要ない、となります。

が、PHPのWeakrefはbeta扱いのモジュールで標準には入っていない機構ですし、使いこなすにはライブラリ設計者に参照を管理する能力が求められます。ちゃんとみんなが使いこなしていれば便利な世界になるのでしょうけど。。

もし今後、Weakrefが標準バンドルされるようなことになったとして、そしてみんなが非常に注意深くプログラミングしても、あっさり循環参照を作ってしまう機構がまだ残っています。PHP5.3から導入されたクロージャです。

クロージャにより崩壊する世界

クロージャを多用するプログラミングを行うと、すぐに循環参照ができあがります。JavaScriptで循環参照を作らないようにプログラミングしろって言われたら私は死ぬと思います。

もちろん、こういう単純な再帰でもクロージャ内に循環参照ができます。

$fact = function ($x) use(&$fact) {
  if ($x <= 1) {
    return 1;
  } else {
    return $x * $fact($x - 1);
};

もっと言えば、PHP5.4以降で以下のクラスはnewするだけで循環参照になります。

class Klass {
  function __construct() {
    $this->f = function(){};
  }
}

というのも、PHP5.4からクラス内で定義したクロージャは自動で$thisをbindするようになったからです。

Klass ← function(){} ← プロパティf ← (Klassに戻る) こんな感じですね。Klassのインスタンスは循環参照コレクタに拾われるか、プロセスが終了するその時まで解放されません。

一応$this->f = static function(){}とstaticを書くことで$thisをbindしないようにできます。できますが、どうでしょう。。。そろそろ「循環参照を作らず注意深くプログラミングする」なんてのはかなり難しいことに思えてこないでしょうか?

循環参照だろうが何だろうが動作を保証するfinally

finallyはオブジェクトと無関係な文法です。なので参照カウンタがどうこうというのは関係なく、コードの実行を強制することができます。

参照カウンタを諦めて、常にライブラリ利用側で後始末処理をfinallyで書く、というのは個人的には憂鬱なのですが、このままPHPが循環参照上等のクロージャ多用型プログラミングに突き進むのであれば、finallyはなくてはならないものになっていくと思います。

まとめ

  • デストラクタの天敵、循環参照
  • クロージャが導入され、循環参照を作りやすくなってしまった。デストラクタの信頼が揺らぎつつある
  • 参照関係なしに処理を強制できるfinallyは、今後切り札として必要になってくるだろう

ただ、finallyがあるから「利用側で後始末しろ」ってなるのはどうかなーと思います。。ライブラリ設計者はなるべく正しくデストラクタを使い、利用側に不便をかけないのが正しい姿勢でしょう。

循環参照を避けて行儀よくプログラミングしていくか、クロージャの利便性を取ってRAIIを捨てるか。ちょっと極論ですが、道を選べるようになってきたのかもしれません。

参考

__del__, gc, 循環参照, weakref Pythonの場合の話。どこの言語でも悩ましいポイントなんですかね。


PHPの最新記事