Perlにおける循環参照
Perlにおける「循環参照」は, あるリファレンスからリファレンスをたどっていった際, 最初のリファレンスに戻ることができる状態のことを言います.
...といってもわからないので, 実例を見てみましょう:
use strict;
use warnings;
my $hash_ref = { a => 1, b => 2 };
my $array_ref = [ $hash_ref, 1, 2, 3 ];
$hash_ref->{array} = $array_ref;
print "$hash_ref\n";
print "$hash_ref->{array}->[0]\n";
このコードを実行すると, 次のようになります.
HASH(0x7f8f399b3670)
HASH(0x7f8f399b3670)
これは, $hash_ref
と$hash_ref->{array}->[0]
が同じものを指しているということであり, つまりは$hash_ref
から$hash_ref->{array}->[0]
とリファレンスをたどっていくことで, 最初の$hash_ref
が参照できる, ということを意味します.
循環参照や, それによって生じるメモリリークについては, 2011年の資料ではありますが @hiratara さんの循環参照のはなしというスライドも参考になりますので, 見てみると良いと思います.
循環参照の発見
こういった循環参照の発見には, Devel::Cycleモジュールを使うのが便利です.
先ほどのコードの場合,
use strict;
use warnings;
use Devel::Cycle;
my $hash_ref = { a => 1, b => 2 };
my $array_ref = [ $hash_ref, 1, 2, 3 ];
$hash_ref->{array} = $array_ref;
find_cycle($hash_ref);
このコードを実行すると, 次のような出力が得られます.
Cycle (1):
$A->{'array'} => \@B
$B->[0] => \%A
$hash_ref
には, 1つの循環参照が存在する, というわけです.
循環参照の解決
この循環参照を解決する方法の1つが, 「リファレンスを弱くする」ことです.
詳しい説明は省きますが, 循環参照のうち, 1つのリファレンスを「弱く」することでメモリリークを防ぐことができます.
リファレンスを弱くするためには, 次のようにScalar::Utilのweaken
を利用します.
use strict;
use warnings;
use Devel::Cycle;
use Scalar::Util;
my $hash_ref = { a => 1, b => 2 };
my $array_ref = [ $hash_ref, 1, 2, 3 ];
$hash_ref->{array} = $array_ref;
print "before:\n";
find_cycle($hash_ref);
Scalar::Util::weaken($hash_ref->{array});
print "after:\n";
find_cycle($hash_ref);
実行結果は次の通りです.
before:
Cycle (1):
$A->{'array'} => \@B
$B->[0] => \%A
after:
Scalar::Util::weaken($hash_ref->{array});
で, $hash_ref->{array}
が示す, 配列へのリファレンスが弱められています.
そのため, リファレンスを弱くする前(before:
の部分)は循環参照が生じていますが, その後(after:
の部分)は何も表示されておらず, 循環参照が解決されていることがわかります.
オブジェクトと循環参照
こういった循環参照が起きやすい場面の1つが, 「子のオブジェクトが親のオブジェクトを参照する」というシチュエーションです(今回こういった記事を書いたのは, お恥ずかしながらまさにこのシチュエーションでハマってしまったからです...).
次のコードについて考えます:
package Parent;
use Mouse;
has child => (
is => 'ro',
isa => 'Child',
default => sub { Child->new( parent => $_[0] ) },
);
package Child;
use Mouse;
has parent => (
is => 'ro',
isa => 'Parent',
);
Parentをnew
すると, 自動的にChildもnew
され, このChildはParentからchild
メソッドで参照することができるようになります.
更に, Childはparent
メソッドで自分自身を生成したParentを参照することができます.
Parentのオブジェクトを生成して, Devel::Cycleで検証してみましょう.
use strict;
use warnings;
use Devel::Cycle;
my $parent = Parent->new;
find_cycle($parent);
次のように, ParentとChildで循環参照が発生していることがわかります.
Cycle (1):
$Parent::A->{'child'} => \%Child::B
$Child::B->{'parent'} => \%Parent::A
この循環参照も, 先ほどと同じようにScalar::Utilのweaken
で解決できます.
use strict;
use warnings;
use Devel::Cycle;
use Scalar::Util;
my $parent = Parent->new;
print "before:\n";
find_cycle($parent);
Scalar::Util::weaken($parent->child);
print "after:\n";
find_cycle($parent);
実行結果は次の通りです.
before:
Cycle (1):
$Parent::A->{'child'} => \%Child::B
$Child::B->{'parent'} => \%Parent::A
after:
余談: Mouseのweak_ref
Mouseでは, 次のようにweak_ref
を指定することで, weaken
を適用した時と同じ状態を作ることができます.
package Parent;
use Mouse;
has child => (
is => 'ro',
isa => 'Child',
default => sub { Child->new( parent => $_[0] ) },
);
package Child;
use Mouse;
has parent => (
is => 'ro',
isa => 'Parent',
weak_ref => 1, # 弱いリファレンスにする
);
package main;
use strict;
use warnings;
use Devel::Cycle;
my $parent = Parent->new;
print "result:\n";
find_cycle($parent);
実行結果は次の通りで, weaken
を使うことなく, 弱いリファレンスによって循環参照が解決されていることがわかります.
result:
さいごに
Perlにおける循環参照と, Scalar::Utilのweaken
を利用した解決法について書きました.
リファレンスはPerlを学習する一つの「山」ですが, これが理解できれば複雑なデータ構造をPerlで表現出来るようになり, Perlで出来ることが一気に広がります.
とはいえ気をつけないと, このような「循環参照」によって問題が生じてしまうので, 頭の片隅に置いておくと良いのではないでしょうか.