2008年07月06日 19:30 [Edit]

perl - 暗黙的な参照 # @_ と $_

camel

Good Question!

@_の要素の$_[0],$_[1]等は、格納するべきアドレスが可変! - 燈明日記
なぜ、こんな仕様にしたのだろうか・・・。

Good Question だけあって、Good Reasons もきちんとあります。


@_と$_の秘密

まず、事実を再確認しておきましょう。

Perlにおいて、引数を格納する配列@_は、常に参照(reference)です。値(value)ではありません。

これが何を意味するかというと、@_への書き込みが、呼び出し元への書き込みとなるということです。

実際に様子を見てみましょう。

[Run via CodePad]
sub inc { ++$_[0] }
my $a = 0;
print $a, "\n";
inc($a);
print $a, "\n";

これは、引数に常に値が入るJavaScriptなどと異なります。

var inc = function(n){ return ++n };
var a = 0;
alert([a, inc(a), a].join(', '));

JavaScriptで同様の結果を得るには、次のようにして呼び出し側で明示的に代入する必要があります。

var inc = function(n){ return ++n };
var a = 0;
alert([a, a = inc(a), a].join(', '));

Perlでは、他にも、foreachgrepmapのブロック中の$_も参照となっています。

[Run via CodePad]
local ($, , $\) = (', ', "\n"); # for print
my @a = (1..10);
print @a;
--$_ foreach @a;
print @a;
map { $_ *= 2 } @a;
print @a;
print grep { $_ *= $_ } @a;
print @a;

二つの理由

なんで、こうなっているのでしょうか。理由は二つあります。

一つは、歴史的な理由。Perl 4まで、Perlには明示的な参照(reference)という仕組みがありませんでした。$$scalarref = 'new string';のようなことが出来なかったのです。

もう一つは、効率。たとえばfunksun($scalar)$scalarが、何MBもあったらどうなるでしょう?関数の呼び出しのたびに、その何MBもある内容がコピーされる羽目になります。参照渡しであれば、それを防ぐことが出来ます。

とはいえ、意図せず参照先を改変してしまうのは危険です。メモリーも安く、my(レキシカル変数)もサポートされた今では、

my $self = shift; # 引数一つuse Data::Dumper;
my %hash = do { my $i = 0; map { $_ => $i++ } qw/Sun Mon Tue Wed Thr Fri Sat/ };
print Dumper \%hash;
while(my ($k, $v) = each %hash){ $k = lc($k); $v *= 2 };
print Dumper \%hash;

my ($a, $b, $c) = @_; # 引数全部

という具合に、最初に引数のコピーを取った上で、そのコピーを利用するというのが作法(a good practice)となり、参照元を改変する場合には、呼び出し側で明示的に

my $a = 0;
inc(\$a);

とした上で、関数側でも明示的に

sub inc{
  my $n = shift;
  $$n += 1;
}

という風にするようになってきましたが、今でも引数に代入しなければならない場面というのは稀ながら存在します。例えばEncodeでも、オプションの指定によってはエラー処理の際、未処理部分を引数に書き戻すようにしています。これがなぜ必要なのかは、Encode::PerlIOをご覧下さい。

@_$_のこの特性は、知らずに使うとちょっと危険ですが、知っていれば効率的なコードを書けます。上手に利用しましょう。

そうそう。もう一つ留意点。foreach my $elem (@array){ ... }$elemも参照となります。

[Run via CodePad]
local ($, , $\) = (', ', "\n"); # for print
my @array = (0..6);
for my $i (@array) { $i++ };
print @array;

これに対して、while(my ($k, $v) = each %hash){ ... }$kおよび$vはコピーです。

[Run via CodePad]
use Data::Dumper;
my %hash = do { my $i = 0; map { $_ => $i++ } qw/Sun Mon Tue Wed Thr Fri Sat/ };
print Dumper \%hash;
while(my ($k, $v) = each %hash){ $k = lc($k); $v *= 2 };
print Dumper \%hash;

Dan the Perl Monger


この記事へのトラックバックURL

この記事へのコメント
項「二つの理由」の直後のコードで
Data::Dumperたんがコメント部でさぼってますよ!
Posted by 重箱野郎 at 2008年07月09日 00:37
こんなの全然「良い」理由じゃないですよ。
perlのこういうカオス、一つ一つは確かに些細なカオスが集まって
巨大なソースツリーになると大変な事になってしまうではありませんか。
Posted by airscope at 2008年07月06日 22:53