先日、Perlのプログラムがうまく動かないってことで相談受け、でそのバグはとれたのだけど、なぜ元々のソースでは動いてたのかがわからない、という状況になりました。
そこで僕のPerl師匠 h小林 @hkoba さんに相談したところ、自分はこんな基本的なところをわかってなかったんだ… と目からウロコなことがありました。
それはPerlの「my」は変数の宣言だけではなく、オブジェクトの「new」の意味も含むということです。
実際のソース出すこと出来ないので、大元になってる HTML::Template のソースを使って説明します。
HTML::Template - CGI スクリプトから HTML テンプレートを使うための Perl モジュール - perldoc.jp
より、TMPL_LOOPを使うためのサンプルコード
# a couple of arrays of data to put in a loop: my @words = qw(I Am Cool); my @numbers = qw(1 2 3); my @loop_data = (); # initialize an array to hold your loop while (@words and @numbers) { my %row_data; # get a fresh hash for the row data # fill in this row $row_data{WORD} = shift @words; $row_data{NUMBER} = shift @numbers; # the crucial step - push a reference to this row into the loop! push(@loop_data, \%row_data); } # finally, assign the loop data to the loop param, again with a # reference: $template->param(THIS_LOOP => \@loop_data);
HTML::Templateを使ったことがある人なら、結構見たことがあるコードではないでしょうか。
一旦ハッシュ %row_data に項目を保存して、そのハッシュのリファレンスを配列 @loop_data に突っ込んで、templateに渡してやるという作りになっています。
ここで注目して欲しいのが
while (@words and @numbers) { my %row_data; $row_data{WORD} = shift @words; $row_data{NUMBER} = shift @numbers; push(@loop_data, \%row_data); }
という部分です。
自分は「my」を、Cとかコンパイル言語でのintやcharのように「変数の宣言」として理解していました。
その理解でこの部分を読むと、このwhileループの中で作られたハッシュ %row_data は毎ループ同じものが使われるはずです。
そしてループするたびに新しく @words と @numbers から取得したデータを入れられて、それが @loop_data へpushされるのですが、リファレンスで渡されるため、毎回同じハッシュを指すリファレンスが突っ込まれることになります。
すると @loop_data から値を参照すると @words と @numbers の最後の値がループ個数分入っている、という結果になるはずです。
でも実際にはそうはなりません。
@loop_data からはちゃんと、@words と @numbers が個別に参照できるはずです。
もっと単純なCとperlのソースで、動きを比較してみましょう。
int i ; for (i = 1; i <= 3; i++) { int x ; x ++ ; printf( "%d ", x ); }
Cだと「1 2 3」という結果になりますね。(gcc-4.2の場合。ほんとはこのコードはxが初期化されるかどうかは不定なので必ずこういう結果になるとは限らない)
for (my $i = 1; $i <= 3; $i++) { my $x ; $x ++ ; print "$x " }
perlでは必ず「1 1 1」になるのです。
これは「my」が単に変数の宣言ではなく、変数がそこで「new」される、つまり新たにオブジェクトが作られるという違いがあるためです。
なのでperlでも下記のようにmyの行をループの外に出してしまえば「1 2 3」という結果になります。
my $x ; for (my $i = 1; $i <= 3; $i++) { $x ++ ; print "$x " }
このように「my」で「new」されることを意識せずに書いていると、ぱっと見、影響が無さそうな変更が大きな違いを生むことになってしまうわけです。
また、myで宣言されたものはスコープの中のローカル変数(オブジェクト)なので、そのスコープが消えたら普通削除されるのですが、リファレンスが残っている場合は削除されません。
push(@loop_data, \%row_data) で、ループの先頭で毎回新たにnewされた %row_data へのリファレンスが保存されているため、ループ毎にハッシュが保持されていくことになります。