WEBアプリでありそうなこんなコードがあります。flushでデータを排出するのですが、そのときに$app->run内で$app->{flush_fanc}に関数リファレンスを渡すと、$app->flushでそれを終了処理として呼びます。
package main;
{
my $app = App->new();
$app->run();
$app->flush();
}
print "end?n";
さて、Appクラスのrunを実装します。このとき、flush時に依頼する終了処理としてAppクラスの"メソッド"を使いたい場合、単純な関数リファレンスだとメソッドとして渡せないので、クロージャを渡してみます。ここでは、終了処理としてhelloメソッドを、きちんと$selfのメソッドとして呼び出したいとします。
package App;
sub new{bless{}, shift}
sub hello{print "hello?n"}
sub run{
my $self = shift;
#いろーーんな処理 ===
#...
#終了時にやりたい処理があるので、依頼
$self->{flush_func} = sub {
#クロージャとして、メソッドを実行する
$self->hello();
}
}
sub flush{
my $self = shift;
$self->{flush_func}->();
}
sub DESTROY{print "DESTROYED?n"}
1;
はい、こんな感じで。なぜかDESTROYがあるのは、実行結果をみるため。実行結果は次のような感じです。
hello
end
DESTROYED
気づいたでしょうか? $appのスコープはブロック内なので、本来はendの前にDESTROYEDと表示されなければならないのに、最後に表示されている。これは、$appが破壊されずにプログラムの終了まで生き延びた、つまり、リークしてると言うことです。
するどい人ならこの結果を見る前に気がついたと思いますが、クロージャに$selfを使っていて、それを$selfのメンバとして保存しているため、循環参照が起こっているわけです。
でもって、この解決法。恥ずかしながら昨日初めて知ったのですが、Scalar::Util::weakenなんていうなんとも破天荒な関数が居ます。これは、参照カウンタを1減らすと言うもの*1。
$self->{flush_func} = sub {
$self->hello();
Scalar::Util::weaken($self);
}
これでめでたくクロージャ内の$selfは弱いリファレンスとなり、循環参照によるリークを防げるようになります。
*1:ほんとはカウンタを増やさないリファレンスだが、コードを書いてて「減らす」と考えた方が直感的かな、と。