Hatena::ブログ(Diary)

TrashSUITE このページをアンテナに追加 RSSフィード Twitter

2010.04.14(Wed)

C/Objective-C + Blocks でクロージャ

| 22:35 |  C/Objective-C + Blocks でクロージャを含むブックマーク  C/Objective-C + Blocks でクロージャのブックマークコメント

Blocks というのは AppleC/C++/Objective-C 向けに独自実装したクロージャ的な機能.Snow Leopard リリース時に GCD すげぇ!という記事で見かけて『Apple やりやがった,あの変態やりやがった』くらいにしか思っていなかったのだけど,iPhone OS4.0 で使うことになりそうなので,ザックリと調べてみた

ザクザクっとドキュメントを読みつつテストしつつという状態で書いたメモなので,嘘を書いている可能性が大いにある!

以下,Blocks をクロージャと呼ぶ

シンプル

// 無名でダイレクトに呼ぶ
^(void) {puts("closure");}(); //=> closure
    ^() {puts("closure");}(); //=> closure
       ^{puts("closure");}(); //=> closure

// 名前を付けて呼ぶ
void (^shout)(char*) = ^(char *string) {printf("%s!!!\n", string);};
shout("call");   //=> call!!!
shout("blocks"); //=> blocks!!!

宣言的には関数ポインタの * が ^ になったと思えばまぁアリかなという感じ.右辺のロジックに関しても,既存のクロージャに慣れていれば,あぁ無名関数だなという感じだ.違和感はない…わけねぇだろ.キモいよ,キモすぎるよ…と思った僕だけど,最近 Haskell で遊んでいたせいか,己の既成概念的に見て変態的構文でも,ある程度『仕方ないね』で済ませられるようになってきた.よってこれも『仕方ないね』で受け入れようと思う.見聞は広めておくにこしたことはない!人間,明日のことなどわからないもので,慣れてしまえば快感になるかもしれない!

引数を取らないクロージャの場合,^{...} と書くのがスッキリして僕は好きだなぁ

スコープの外の変数アクセス

        int ro = 0;
__block int rw = 0;

^{
  ro++; //=> error: increment of read-only variable ‘ro’
  rw++; //=> 1
}();

クロージャの内部から,スコープの外の変数アクセスすることができる.ただし,通常は参照することしかできない.書き換えが必要な場合は,対象となる変数を __block 付きで宣言しなくてはならない.__block なしで宣言した変数を書き換えようとした場合,コンパイルエラーとなる

関数引数クロージャを渡す

// 第二引数にクロージャを取る eachString 関数を定義
void eachString(const char *string, void (^block)(const char c)) 
{
  for (int n = 0; string[n] != '\0'; n++)
    block(string[n]);
}

int main(int argc, char **argv)
{
  // クロージャをダイレクトに記述して呼ぶ
  eachString("oppai", ^(const char c) {
    printf("%c", toupper(c));
  });  //=> OPPAI

  return 0;
}

上記のように,ダイレクトクロージャ記述して渡すことができる.できるというか,個人的にはこれがクロージャの真骨頂だと思っているので,できなければ旨みが半減どころの話じゃあないわけだけど.もちろん名前を付けて渡すこともできる

クロージャ関数ポインタを使って実装されている

つまりコンテキストデータスタック上にあるわけで,スコープを抜けると破棄されてしまう.なので,クロージャを返す関数などのようにスコープをまたいで持ち回したい場合には Block_copy() を使ってヒープへコピーしてやらなければならない.ヒープへコピーしたクロージャを破棄するには Block_release() を使う

#include <Block.h>

// これはアリ
int (^counter(int init))(int)
{
  __block int n = init;

  return Block_copy(^(int x) {
    n += x;
    return n;
  });
}

// これは NG
// error: returning block that lives on the local stack
int (^counter(int init))(int)
{
  __block int n = init;

  return ^(int x) {
    n += x;
    return n;
  };
}

// ヒープへコピーしたクロージャを受け取る
int (^copied_closure1)(int) = counter(0);
int (^copied_closure2)(int) = counter(0);

// 使ってみる
copied_closure1(1); //=> 1
copied_closure1(1); //=> 2
copied_closure1(1); //=> 3

copied_closure2(1); //=> 1
copied_closure2(1); //=> 2
copied_closure2(1); //=> 3

// 使い終わったら破棄する
Block_release(copied_closure1);
Block_release(copied_closure2);

検証コードは載せていないけれど,どうやら Block_copy() でヒープへコピーされるのは __block 付きで宣言された変数だけらしい.それ以外の変数クロージャを呼ぶ度にスタック上へ生成/破棄される

クロージャスコープ外の変数を参照していなければコンパイルエラーにはならないが…

// Block_copy を使わなくてもコンパイルできるケース
void (^badCounter())(int)
{
  // スコープ外の変数を参照していないので OK
  return ^(int x){
    static int n = 0;
    n += x;
    printf("closure %d\n", n);
  };
}

// ただし…
// 独立したクロージャを複数生成したつもりで,
void (^closure1)(int) = badCounter();
void (^closure2)(int) = badCounter();

// 同一のクロージャを指すポインタを使っていたりする
printf("%p\n", (void*)closure1); //=> 0x100001080
printf("%p\n", (void*)closure2); //=> 0x100001080
closure1(3); //=> 3
closure2(4); //=> 7

パフォーマンスを重視した戦略なのか,関数を呼ぶ度に独立したクロージャを生成するようなことはせず,毎回同一のクロージャを指すポインタが返るので,意識せずに使うとバグを仕込む可能性が高い

テストしながら書いたのでまとまりに欠けるのがアレだけど,とりあえず今のところそんな感じ.objc の idオブジェクトと組み合わせて使うと面白そうな気がするなぁ

リンク元