色々ややこしいBlocks。
宣言の仕方から、内部実装まで色々と調べたりしたのでメモ。
##基本形
まずは基本形。
void (^blk)(void) { NSLog(@"in block"); };
これをベースに展開していく、と覚えておくといい。
Blockは省略できる部分もある。
上記は以下のように省略できる。
(^blk)() { NSLog(@"in block"); };
voidだから、ではなく、Block内のreturn
文を読み取ってその値を返り値の型としてくれるらしい。
ちなみに、関数型ポインタの変形として覚えると覚えやすいかも、と思った。
###関数型ポインタの例
int func(int i) {
return i + 1;
}
int (*funcptr)(int) = &func;
このint (*funcptr)(int) = &func;
という部分。
*
を^
にすればBlockになるイメージ。
##型宣言
多分、実装する場合は宣言をして使うのが基本になると思う。
typedef void (^blk_t)(void); //`blk_t`が型となる
実際に使う場合は以下。
blk_t blk = ^{ NSLog(@"in block"); };
##メソッドの引数宣言
ここがややこくなるところ。
- (void)hogeMethod:(void (^)(NSString *str))blk
{
NSString *test = @"argument";
blk(test);
}
基本的からだいぶ離れてしまった。
けど、Objective-Cのメソッド引数名はシグネチャのあとに来る、ということを考えると、こんな感じで考えるといいのではないかと。
まず最初にBlockの変数名が最後に来る。その後で、そのBlockの定義を基本形を元に書いていく。
ただ、すでに変数名は最後に書いてしまったので、そこだけ消去する。
そうするとメソッド引数の形が見えてくる。
ちなみに、型部分「void (^)(NSString *str)
」はかっこでくくる必要がある。
分かりやすくすると↓な感じ。
// [Blockの型]には`void (^)(NSString *str)`が入る
- (void)hogeMethod:([Blockの型])blk
{
//
}
##Block定義
こっちもだいぶややこい。
void (^anyBlk)(int) = ^void (int i) {
printf("%d", i);
}
anyBlk(10);
=
の左側はいわゆる変数宣言。(int count = 5;
とかと同じ)
ただ、その型部分がややこしいためとても複雑に見える。
型のみにすると
void (^)(int i) = …
が型。確かに戻り値と引数の型をしっかりと宣言している。
そしてキャレット(^
)の後に来るのが変数名、というわけ。
ただ、その型に代入する値としてのBlockもまた変な形なのでさらにややこしい。
これは、上記の「型」を持った値を代入している、と考えると少しは分かりやすいかも。
void (^anyBlk)(int i) = ^void (int i) ...
頭の^
はポインタの*
のようにBlock型を表すものと考えればいいと思う。
で、最後に実際の関数部分を{}
の中に書く、と。
##Blockを関数の戻り値にする
基本的にはtypedef
を使って型を定義しておいたほうが可読性が上がります。
typedef int (^blk_t)(int);
blk_t func() {
return ^(int i) { return i * i; };
}
ただ、型定義をしなくても一応使えるけど、もうなにがなんだかわからなくなるw
int (^func())(int) {
return ^(int i) { return i * i; };
}
これは、int (^)(int)
という型のBlockを返す関数です。つまり型定義したほうと同じ。(関数名はfunc
)
Blockを知らずにfunc
が関数名だと自信を持って言える人はいないと思います・・。
もはや宣言なのかすら怪しい状態です。やはり素直に型定義して使うほうがいいでしょう。
定義だけでもややこしいけど、コンパイラが解釈する内部実装はさらに変態的になっている模様・・。
ちょっと説明できる自信がないけど、分かっている範囲で備忘録として書いておこうと思います。
##Blockの実装
「エキスパート Objective-C プログラミング」という書籍が実装についてとても詳しく解説してくれているので、そちらを読むと理解が早いと思います。
さて、Blockは大雑把に言えば(最終的に)ただの構造体です。
コンパイラが色々とややこしいところをすべて受け持ってくれて、やりたいことだけを記述できるようになっている、というわけです。
###自動変数のキャプチャ
Block外で宣言された変数を、Block内で使用することができます。
これは「自動変数のキャプチャ」という機能があって、これによって実現しています。
「キャプチャ」という名前から想像できるように、Block内部では実際の変数ではなくコピーが使われます。
####キャプチャの例
int i = 5;
void (^capTest)() = ^{
printf("%d", i);
};
i = 10;
capTest(); //=> 5になる
i
に10を代入してから実行していますが、結果は5
になります。
これは、Blockが定義された時点でのi
の値を「キャプチャ」しているために起こります。
###__block修飾子
さらに、もしBlock内部でその変数を変更したい場合は__block
修飾子を付けて宣言する必要があるのです。
さて、ではなぜこんなことが必要なんでしょうか。
コピーと書いたように、コンパイラはBlockを構造体として表します。
そしてその構造体を生成する際に、内部で使用している変数の値を「キャプチャ(コピー)」して、その構造体のメンバにします。
(構造体には関数へのポインタや、関数内で使用する変数を保持するメンバがあります)
このコピー処理のために関数内で問題なく使えると同時に、変更ができなくなるわけですね。
(コピーしたものをいくら変更しても意味がない)
Blockを使ったコードをC++に変換してみると、以下のような構造体が作られます。
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_i_0 *i; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
まだしっかり理解できてないのでコードのみの紹介ですが、Blockが構造体になる、という雰囲気が分かると思います。
さて、こうした複雑な仕組みのためにキャプチャが行われているわけです。キャプチャではなく、変数を自由に操作したい場合に必要なのが__block
修飾子です。
さて、では__block
を付けた変数はどうなるのでしょうか。
(ここはちょっとしっかり説明できる自信がありませんが)コンパイラはblock変数用の構造体を作ります。
そして、Blockを生成している関数内で該当の変数にアクセスがあった場合(つまりBlock外)も、Block内でアクセスがあった場合も問題がないように、変数へのポインタを制御してアクセス可能にします。
###変数スコープを超えて
Blockは値であるので、当然、引数に取ることも、関数の返り値にすることもできます。
ここでひとつ疑問がわきます。
とあるメソッドでBlockを返した場合、それがどこで使われるか分かりません。
さらに、そのBlock内ですでに終了した関数内の変数にアクセスした場合はどうなるでしょうか。
本来であれば、不正アクセスとして終了してしまいます。
しかし当然、Blockを使っている場合はそうはなりません。
Blockが戻り値として変える場合、自動的にBlockがヒープ領域にコピーされるようになっています。
そして先に書いた通り、Blockは構造体であるのでそのメンバを通して適切に変数にアクセスできる、というわけです。
###変換されたソースを見る
実際に変換されたソースを見ると、なにが行われているかが分かると思います。
Blockを使った簡単なコードを書いて、以下のように-rewrite-objc
オプションを指定して変換しているとどうなるかを見ることができます。
clang -rewrite-objc main.m
数行のコードが、数百行まで膨れ上がります。
ちょっと行数が多いのでここには載せませんが、ごく簡単なサンプルを変換してみるとどういう動作をしているかが分かると思います。(上記のBlockの構造体もこれを利用して変換したコードを抜粋したものです)
ごく簡単なサンプルを作って変換してみると、なにがどうなっているのか分かるのではないでしょうか。