Powered By Blogger

2011年4月4日月曜日

iOSでBlocks(クロージャ)1

iOS 4プログラミングブックはBlocksについて分かりやすく書いてある。

●Blocksとは
いわゆるクロージャ。
関数内で宣言できるいわゆる無名関数だが、ただの無名関数ではない。
引数以外に外側のスコープのローカル変数を参照出来る。

前に^が付いているのがBlock構文である。
戻り値がvoidなので省略が可能で
^(int arg) { /* Block処理 */ };
としたが、省略しない場合は
void ^(int arg) { /* Block処理 */ };
となる。
ちなみに引数もvoidなら更に省略できて
^{ /* Block処理 */ };
と記述出来る。

下のコードでは変数fnにBlockを代入し、aとbをインクリメントした後でfnを関数ポインタの様に呼び出している。

//
int a = 1, b = 1;
void (^fn)(int) = ^(int arg) {printf("a=%d, b=%d\n", arg, b);};//(1)
++a; ++b;
fn(a);  //a=2, b=1 と出力される
//
ローカル変数aは引数として渡される直前でインクリメントされているのでa=2と表示される。
一方bはb=1と表示される。
aと同じ行でインクリメントされているのでfn(a)呼び出し時点でbも値は2である。

では何故b=1表示されるのだろうか。
これがクロージャたる所以で(1)でfnにBlockが代入された時点で変数bをキャプチャ(コピー)されるからである。

●キャプチャされたローカル変数はどこに記憶されるのか?
ローカル変数を使っていない場合:.dataセクション
ローカル変数を使っている場合:スタック
上の例はローカル変数をキャプチャするので、この場合ブロックはスタック上に配置される。
このスタック上に配置されるブロックがうっかりすると厄介な間違いの元になる。
★間違った使い方の例

//
typedef void (^block_t)();
  block_t blocks[5];
  int i;
  for(i=0; i < sizeof(blocks)/sizeof(blocks[0]); ++i) //Loop1
    blocks[i] = ^{printf("%d\n", i);};
  for(i=0; i < sizeof(blocks)/sizeof(blocks[0]); ++i) //Loop2
    blocks[i]();

実行結果
4
4
4
4
4

意図した実行結果は以下だったのだが...
0
1
2
3
4

何が間違っているのか?

Loop1のブロック^{printf("%d\n", i);};はループ毎に一時的にスタック上に配置され、そのスタック位置のアドレスは各ループiのblocks[i]に保存される。 ループが進むたびに破棄されスタック位置は元に戻る。 つまりループ毎にブロックはスコープから外れるので、このケースではループの全ての回で同じスタック位置に一時ブロックが上書きされる事になる。 blocks[0]からblocks[4]には結果的に同じアドレスを指しているので実行結果は全て4になるのだが、Loop1を抜けた時にはこのアドレスはダングリングなのでLoop2を実行する事自体が間違っている。

●この問題の回避方法
Block_copyを使ってスタックに一時的に生成されたブロックをヒープにコピーする。

//
typedef void (^block_t)();
  block_t blocks[5];
  int i;
  for(i=0; i < sizeof(blocks)/sizeof(blocks[0]); ++i)
    blocks[i] = Block_copy(^{printf("%d\n", i);});//Blockをヒープにコピー
  for(i=0; i < sizeof(blocks)/sizeof(blocks[0]); ++i){
    blocks[i]();
    Block_release(blocks[i]);//ヒープ上のものは使い終わったらreleaseする
  }

実行結果
0
1
2
3
4

0 件のコメント:

コメントを投稿