RubyエンジニアのためのObjective-C Blocks入門に引き続き、Blocksネタ。そっちの記事ではBlocksはクロージャ的ななにかだって言ってるのに単なる関数ポインタみたいにしか使ってなかったので、せっかくなのでクロージャ的に使ってみる。

eachできるならmapも欲しい

-[NSArray enumerateObjectsUsingBlock:]を使えば、Array#each相当のことができるとこまでは前回の記事でできた。そうすると、Array#map相当のこともNSArrayにさせられるはず。例えばこんなの。

# ruby
array_a = %w(ひたぎ 真宵 駿河 撫子 翼)
array_b = array_a.map {|x| "#{x}が可愛過ぎて生きるのが辛い" }

Rubyistには説明の必要もないと思うけど、Array#mapが何をしてるかというと、

  1. 1引数のブロックを受けて
  2. 自分自身の要素を一つずつブロックに渡して
  3. その返り値を要素にした新しいArrayを作って返す(※自分自身は変更しない)

みたいなところ。さてそれをObjCで実装する。まず、ベタに書くとこんな感じ。

// ObjC
@interface NSArray (Map)
- (NSArray *)mapUsingBlock:(void (^)(id))block;
@end
@implementation NSArray (Map)
- (NSArray *)mapUsingBlock:(void (^)(id))block {
  NSMutableArray *newArray = [NSMutableArray array];
  for (id item in self) {
    id obj = block(item);
    [newArray addObject:obj];
  }
  return array;
}
@end

これはこれでなんかスッキリしてていいんじゃねって気がしてきた。でもせっかくなので、enumerateObjectsUsingBlock:を使って書き直してみる。

// ObjC
- (NSArray *)mapUsingBlock:(void (^)(id))block {
  NSMutableArray *newArray = [NSMutableArray array];
  [self enumerateObjectsUsingBlock:^(id item,NSUInteger idx,BOOL *stop){
    id obj = block(item);
    [newArray addObject:obj];
  }];
  return array;
}

ちょこっと説明する。ブロックは、RubyやPerlのそれと同じく、生成されたコンテキストにある変数を参照できる。上のコードで言うとnewArrayはブロックの中じゃなくて外で宣言されてるけど、ブロックはその時点でのコンテキストを持ってるので、ブロックを生成したスコープで見えるものはブロック内でも同じように見える。

で、使う側ではこんな風に書けるようになる。

// ObjC
NSArray *array = [NSArray arrayWithObjects:@"ひたぎ",@"真宵",@"駿河",@"撫子",@"翼",nil];
NSArray *newArray = [array mapUsingBlock:^(id item){
  return [NSString stringWithFormat:@"%@ー!俺だーッ!結婚してくれー!",item];
}];

おお。それっぽいそれっぽい。

__block

ちょうどタイミング良く昨日弾さんとこで同じような話をしてたんだけど、上の例はしれっとブロックの外の変数に破壊的操作をしてるけど、例えばこういうのはできない。

// C or ObjC
typedef int (^bint)(void);

bint make_incr() {
  int n  = 0;
  bint f = ^int(void){ return n++; };
  return f;
}

ObjCのBlocksの実装だと、何も指定してないとブロックの外の変数は「見える」だけで「変更できない(ブロックの中から再代入できない)」。でも__block修飾子を付けて変数を宣言すると、ブロックの中から変数を変更できるようになる。それから、そのままfを返しちゃうと、関数のスコープ抜けたときにfが消えちゃうので呼び出し側で使えないところに注意する必要がある(試しにやってみたら、実際には最初の一回だけ実行できたものの、二回目を実行しようとしたところでbus errorになった。ブロックが解放されちゃってるはず)。nをブロック内で変更できて、スコープ外でも使えるブロックを返す関数を書くなら、

// C or ObjC
typedef int (^bint)(void);

bint make_incr() {
  __block int n = 0;
  bint f = ^int(void){ return n++; };
  return Block_copy(f);
}

こんな感じになる。ちなみに、このコードはObjCで書かれてる部分が無いのでMacOSX 10.6だったらCのコードとしてもObjCのコードとしてもコンパイルできるけど、ObjCの場合は<Foundation/Foundation.h>を、Cの場合は<Block.h>をimoport(include)する必要がある。あと、Block_copyして関数のスコープ外で使えるようにしたときは、当然呼び出し側で責任持ってBlock_releaseしてブロックを開放してやる必要がある。

// Cの場合
BLOCK_TYPE block = ^{ ... };
Block_copy(block);
...
Block_release(block);

// ObjCの場合
// Cと同じやりかたでも問題ないし、
// copyメソッドがBlock_copyと、releaseメソッドがBlock_releaseと対応してるので
// そっちも使える
BLOCK_TYPE block = [^{ ... } copy];
...
[block release];

// あとautoreleaseも使える
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
BLOCK_TYPE block = [[^{ ... } copy] autorelease];
...
[pool release]; // ここでblockも開放される

ちなみになんでNSMutableArrayの方は破壊的な操作をできるのかというと、多分変数の操作じゃなくてオブジェクトへのメッセージ送信をしてるだけだから。[newArray count]とかやるのと意味的には同じなのでできるってことじゃないのかな。で、newArray = ... とか変数自体をいじろうと怒られるはず。

次回予告

Grand Central Dispatchで遊んでみた編。多分。