いろんな言語でのクロージャー

Google Maps APIでは、意識してクロージャーを使う機会が多い。例えば、多数のマーカーに対して、ループ回して処理を行う際などに使うのだが、ちょっと躓いたのでメモ。

例えば、以下のようなjavascriptのコードを考えてみる。

var numbers = new Array();
for(var i = 0;i < 3;i++){
numbers.push(function(){
alert(i);
});
}
numbers[0]();
numbers[1]();
numbers[2]();

何となく、numbers配列にpushされる無名関数内のiには、ループカウンタiの、実行時の値が束縛されると思い込んでいた。つまり、このコードを実行すると「0」「1」「2」と表示されると。しかしながら、実際には3回とも「3」が表示される。

これは、javascriptのループでクロージャーを使う際に間違えやすい例として有名な話らしく、ググるといっぱい出てくる。*1結局、無名関数中のiは、ループカウンタであるiが「コピー」されるのではなく、ループカウンタであるiへの「参照」ということらしい。なので、ループ完了時のiの値「3」が表示されてしまう。

ちなみに、正しく動かすには以下のようにしてやればよい。もう一段関数スコープで括ってやり、ループカウンタであるiを引数として渡すことで、numにiの値を束縛することができる。

var numbers = new Array();
for(var i = 0;i < 3;i++){
(function(num){
numbers.push(function(){
alert(num);
});
})(i);
}
numbers[0]();
numbers[1]();
numbers[2]();

ちなみにPerlでは

ちなみにPerlのforでは、ループごとにスコープ(新しい環境)が作られるという記載があったので、試してみた。*2

my @numbers;
for my $i (0 .. 2){
my $obj = {};
$obj->{func} = sub { print $i."\n"; };
push(@numbers,$obj);
}
$numbers[0]->{func}();
$numbers[1]->{func}();
$numbers[2]->{func}();
実行結果:
01
2

お~、確かに0,1,2が表示されるね!と思ったのだけど、以下のコードだとなぜか結果がすべて3になる。

my @numbers;
#for my $i (0 .. 2){
for (my $i=0;$i<3;$i++){
my $obj = {};
$obj->{func} = sub { print $i."\n"; };
push(@numbers,$obj);
}
$numbers[0]->{func}();
$numbers[1]->{func}();
$numbers[2]->{func}();
実行結果:
3
3
3

ループの書き方によって、新しい環境ができる/できないがあるみたい。要調査。

pythonでは

pythonでの実行結果は、以下のとおり。

結果はすべて「2」になるが、基本的にjavascriptと一緒。

import sys
numbers = []
for i in range(0,3):
numbers.append(lambda : sys.stdout.write((str(i) + "\n")))
numbers[0]()
numbers[1]()
numbers[2]()
実行結果:
2
2
2

なので、(javascriptの例にあわせて)こんなことしてやれば、想定どおりの動きになるかな。汚いコードだけど(笑)

import sys
numbers = []
for i in range(0,3):
(lambda num: numbers.append(lambda : sys.stdout.write((str(num) + "\n"))))(i)
numbers[0]()
numbers[1]()
numbers[2]()
実行結果:
01
2

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です