Objective-Cのブロック

Friday, March 30th, 2012

正確に言うと「ブロック」はObjective-CではなくAppleがC言語に標準として追加することを提案している機能です。ブロックとは何かというと、最近の言語だとだいたい含まれている「クロージャ」のことです。(だったら、「クロージャ」という名前にすればよいのだと思うのですが、なぜか「ブロック」という非常に一般的な言葉を選んでいます。さらに、C言語ではすでに{}で囲まれたところをブロックと呼んでいると思うのですが…)
参考にしたのは、以下のAppleの資料です。

ブロックの基本

C言語では昔から関数ポインタがあり、関数のポインタを別の関数の引数に渡したりすることができました。ただ、そのためには、関数を定義し、それを関数ポインタとして渡すという二段階の手順が必要でした。対して、RubyやC#のようにラムダ式やクロージャをインラインで定義できると、ちょっとした数行の処理を関数に渡したりすることができ便利です。

ブロックは、ブロックをコード内にインラインで定義する機能を持っています。そして、その定義したブロックを関数に渡すことができます。実際の例を以下に示します。

int calc(int a, int b, int(^f)(int, int)) {
  return f(a, b);
}

int main(int argc, const char * argv[])
{
  @autoreleasepool {
    int result1 = calc(1, 2, ^(int a, int b) {
      return a + b;
    });
  }
}

構文が今までみたことないようなものなので、少しわかりずらいですが、calc関数の呼び出しの第三引数である以下がブロックです。先頭の^がブロックであることを示しています。

^(int a, int b) {
  return a + b;
}

足し算をするブロックを定義し、それをすぐにcalc関数に渡しています。今までのC言語の関数ポインタでは、add_funcといった名前の関数を定義し、その関数ポインタをcalc関数に渡すという処理が必要でしたが、ブロックでは定義と引数として渡す処理が一緒にできます。

多くの場合は、インラインでブロックを書くことになりますが、関数ポインタのように、ブロックの参照(ブロック参照)を取得し、それを関数の引数に渡すようにすることもできます。

int calc(int a, int b, int(^f)(int, int)) {
  return f(a, b);
}

int main(int argc, const char * argv[])
{
  @autoreleasepool {
    int(^add_func)(int, int) = ^(int a, int b) {
      return a + b;
    };
    int result1 = calc(1, 2, add_func);
  }
}

main関数内では、最初にadd_funcというブロック参照を定義しています。(非常にわかりづらいですが、これでブロックを格納する変数を定義しています)

int(^add_func)(int, int);

これで、int型の2つの引数を受け取り、intを戻すブロック参照を定義したこととなります。そして、単純にブロックをこのブロック参照に代入し、そのブロック参照を別の関数の引数に渡すことができます。

注意として、ブロックの定義”int(^f)(int,int)”では、最初のintで戻り値の型を明示していますが、ブロックでは”^(int, int)”のように戻り値の型は指定しません。コンパイラがブロック定義より正しい戻り値の型を推測します

ブロックを用いて、同じ関数に違う処理をさせる例を示します。

int foreach(int data1[], int data2[], int n, int(^f)(int, int)) {
    int result = 0;
    for(int i = 0; i < n; ++i) {
        result += f(data1[i], data2[i]);
    }
    return result;
}

int main(int argc, const char * argv[])
{
    @autoreleasepool {
        int data1[] = {1, 2, 3, 4, 5};
        int data2[] = {2, 4, 6, 8, 10};
        // add
        int result1 = foreach(data1, data2, 5, ^(int a, int b){
            return a + b;
        });
        // sub
        int result2 = foreach(data1, data2, 5, ^(int a, int b){
            return a - b;
        });
    }
}

ブロックと変数

ブロックでは、他の言語のクロージャやラムダ式と同様に、ブロックが定義された場所で可視な変数を使うことができます(これを変数のキャプチャという)。

int foreach(int data[], int n, int(^f)(int)) {
    int result = 0;
    for(int i = 0; i < n; ++i) {
        result += f(data[i]);
    }
    return result;
}

int main(int argc, const char * argv[])
{
    @autoreleasepool {
        int data[] = {1, 2, 3, 4, 5};
        int value = 2;
        int result = foreach2(data, 5, ^(int a){
            return a * value;
        });
    }
}

ここで、main関数内で定義されたvalueをブロック内でキャプチャしています。ただし、value変数は読み取り専用としてしかアクセスできません。書き込みたい場合は、__blockという修飾子を付ける必要があります。

void foreach(int data[], int n, void(^f)(int)) {
    for(int i = 0; i < n; ++i) {
        f(data[i]);
    }
}

int main(int argc, const char * argv[])
{
    @autoreleasepool {
        int data[] = {1, 2, 3, 4, 5};
        int value = 2;
        __block int result = 0;
        foreach(data, 5, ^(int a){
            result += a * value;
        });
    }
}

この例では、resultを__blockを付けて宣言しています。

ブロックとメモリ管理

インラインでブロックを定義し、その場で呼び出しているだけであれば、あまりメモリ管理については気にする必要がありません。ですが、ブロックが活躍するのはコールバックや非同期処理です。これらの場合、ブロックの呼び出しは、定義した所のずっと後という可能性があります。するとキャプチャした変数が、もう解放されて、なくなっている可能性が出てきます。

ブロックに引数として渡された変数やグローバル変数は、当然ながら、ずっとブロック内から参照可能です。また、キャプチャされた変数も問題なく参照できます。

ブロック内で参照されているObjective-Cのオブジェクトは、自動的に参照カウンタ(retain counter)が+1されます。ですので、解放されることはありません。

C++のオブジェクトの場合、__block修飾子付きでスタックに作成された場合は、コピーコンストラクタでコピーが作成され、それにアクセスできます。__blockが無い場合は、constコピーコンストラクタでコピーが作成されます。

上記の変数についてはなんら問題ないことがわかります。注意するのは、スタック上のデータをポインタで参照している場合や、ヒープ上に作成したC/C++のデータをポインタで参照している場合です。これらの場合、ブロックが実行される際にメモリ上のデータが消えていないことは、開発者が保証しなければなりません。