Perlの循環参照とその解決について

  • 11
    Like
  • 0
    Comment

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::Utilweakenを利用します.

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で出来ることが一気に広がります.
とはいえ気をつけないと, このような「循環参照」によって問題が生じてしまうので, 頭の片隅に置いておくと良いのではないでしょうか.