問題
例えばこういうコード。
class Klass {
public function __construct() {
$this->f = function(){};
}
}
PHPのクロージャではuseで指定したものしか基本的には束縛されないんですが、PHP5.4以降では$thisは敢えてuseしなくても暗黙的に束縛されるようになっています。
class Klass {
public function __construct() {
$this->f = function() use ($this) {};
}
}
このコードで問題になりうるのは、クロージャが\$thisへの強参照を持ち\$thisもクロージャへの強参照をもっていて、循環参照になってしまう事です。これによって
- 循環参照コレクタ(PHP5.3以降)がいつか動き出すまで開放されず、それまでメモリを圧迫してしまう(大きなサイズのオブジェクトが循環参照になっているとGC_ROOT_BUFFER_MAX_ENTRIESへ達する前にallocate失敗して死ぬ?)
- 循環参照コレクタがいつか動き出すまで開放されず、それまでデストラクタの起動が遅れる
といった問題が起こりえます。
ところでPHP5.5以下は2016/07/10に公式のセキュリティサポートが終わっていて使っている人も少ないと思うので、ここでは考慮しません。
対策
弱参照を使い強参照を片方向にする
SwiftやObjective-Cにおけるweakプロパティとかunsafe_unretainedプロパティとかみたいなやつですねたぶん。中身読んでないのでよくわかりません。PECLなので別途インストールが必要みたいなんですが使ってる人どれぐらいいるんでしょうかよくわかりません。
必要がなければ$thisを束縛しない
class Klass {
public function __construct() {
$this->f = static function(){};
}
}
とstaticキーワードを使えば暗黙的な\$this束縛を行わないクロージャも作れるんですが、ただでさえfunctionキーワードが長いのにさらにstaticを付けないといけないとかちょっと嫌ですね。
public function __construct(int $n, callable $f) {
$this->n = $n;
$this->f = $f->bindTo(null);
}
public static function of(int $n) {
return new self($n, function () {});
}
もしくは、外から(あるいは内の別メソッドから)貰う場合、プロパティへ入れる前にClosure Closure::bindTo ( object $newthis [, mixed $newscope = "static" ] )メソッドで\$thisを剥がしてstatic化した複製を作る事も出来ます。(外から付いてくるのは\$thisではなく\$thatと呼ぶべき?)
$thisを複製して束縛する
public function __construct(int $n, callable $f) {
$this->n = $n;
$that = clone $this;
$this->f = $f->bindTo($that);
}
複製してしまうと以降の\$thisの変化は反映されなくなりますが、それでも大丈夫な用途であれば。
Stored PropertyではなくComputed Propertyを使う
public function __construct($n)
{
$this->n = $n;
}
public function f()
{
// Klass への参照が必要ならここで use する
// ただし直接 $this を渡すことはできない
$instance = $this;
return function () use ($instance) {
//
};
}
コメント欄より。外からアドホックなクロージャをもらって保持しておく用途では使えませんが、ビルトインなクロージャを使いたい時などに。
プロパティを使わない
Klass内で保持せずに、必要になったら都度毎回クロージャを渡してやるとか。
クロージャを使わない
例えば、クロージャを避ける
まとめ
実際の所、PHPはリクエスト終了時に全てを洗い流して世界を作り直すのでバッチは別としてあんまりまじめに気にしなくても良いケースが多いんじゃないかなという気はします。私が問題に遭遇したのはPHPUnitででかい環境をもったクロージャをぶん回すベンチコードでの事です。あまり詳しくないので、誤りがあればご指摘下さい。