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