As Sloth As Possible

可能な限りナマケモノでありたい

タグ:Objective-C

どうも、「iOS Advent Calendar 2011」5日目担当のfaultierです。つい最近使ったのでNSURLProtocolネタで。

NSURLProtocolって何?

Foundationフレームワークで最初から扱えるプロトコルはhttp、https、ftp、fileの4つ。これ以外のプロトコルでの通信をNSURLConnectionやNSURLDownloadなどで扱う場合や、特定のリクエストに限って特別な処理をしたい場合などに、NSURLProtocolを継承して登録することで使えるようになる。ちなみに、他のアプリからopenURLしたときにアプリを起動させるカスタムURLスキームとはまた別なので注意。こちらはアプリ内でURL Loading Systemを使うときにだけ影響するもの。

使い方

最低限必要なのは、+canInitWithRequest:、+canonicalRequestForRequest:、-startLoading、-stopLoadingの4つ。

まずはこんな感じで、どんなリクエストのときにそのURLProtocolをが処理するのかを決める。

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
    return [[[request URL] scheme] isEqualToString:@"udon"];
}

ここでYESを返すとこのクラスがインスタンス化されて通信処理に進む。NOの場合は他に登録されているURLProtocolのこのメソッドが登録時の逆順に呼ばれて行く。この場合はudonというスキーム、例えばudon://marukame/bukkake/coolみたいなURLへのリクエストの時に処理をすることになる。別にこれは独自のスキームである必要ではなく、httpやfileなどをフックすることもできるし、特定のホストや特殊なヘッダが付いている時だけ処理するようなこともできる。

次は+canonicalRequestForRequest:。リクエストをcanonicalな形に変える必要がある場合はここで弄るのだけど、特に何もすることが無ければrequestをそのまま返してやればいい。

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
    return request;
}

その後実際の通信が始まり、-startLoadingが呼ばれる。NSURLProtocolのオブジェクトは、id<NSURLProtocolClient>のclientと、NSURLRequestのrequestというプロパティを持っているので、requestからどんなリソースが必要なのかを判断して、clientに対してデータを返してやる、という形で通信の中継をする。NSURLProtocolClientのメソッドは大体NSURLConnectionのdelegateと対応しているので、NSURLConnectionでの通信を実装したことがあれば分かるはず。NSURLConnection側でキャンセルされたときはstopLoadingが呼ばれる。簡単な例としては以下のようになる。

- (void)startLoading
{
    // 本来非同期で通信するのだけど、
    // この例では単に文字列データを返すだけなので、
    // その場で返してしまう
    NSData *data = [@"うどんが食べたい" dataUsingEncoding:NSUTF8StringEncoding];
    NSDictionary *headers = [NSDictionary dictionaryWithObjectsAndKeys:
                              @"text/plain", @"Content-Type",
                              [NSString stringWithFormat:@"%d", [data length]], @"Content-Length",
                              nil];
    NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:[self.request URL]
                                                              statusCode:200
                                                             HTTPVersion:@"1.1"
                                                            headerFields:headers];
    // NSURLResponseのオブジェクトを返し、
    [self.client URLProtocol:self
          didReceiveResponse:response
          cacheStoragePolicy:NSURLCacheStorageAllowedInMemoryOnly];
    // データを渡し、
    [self.client URLProtocol:self didLoadData:data];
    // 通信を終了。
    // 通信失敗の場合は -URLProtocol:didFailWithError:を呼ぶ。
    [self.client URLProtocolDidFinishLoading:self];
}

- (void)stopLoading
{
    // この例ではキャンセルしようが無い…
    // 内部で別なNSURLConnectionを使っていたり、
    // NSOperationQueueやGCDなどで非同期処理をしている場合、
    // ここでキャンセルの処理を実行する。
}

これだけだとudonスキームはまだURL Loading Systemに登録されていないので、適宜NSURLProtocolの+registerClass:、+unregisterClass:呼んであげることで使えるようになる。その独自スキームを使うUIViewControllerとか、あるいはもうクラスのloadメソッドで登録してしまうとか。

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [NSURLProtocol registerClass:[self class]];
    });
}

使いどころ

それどういうときに使うの?http以外のプロトコルとかそんな実装しないよ?と思うかもしれないけど、意外と使い道がある。UIWebView等での通信は裏側でNSURLConnectionを使っているので、WebView内での通信をフックしてアプリ側で弄ったりできるのだ。例えばCoreDataにあるデータをJSON形式にして返すようにしておくとか、JSからのトリガーでアプリ内のバックグラウンド処理を走らせるとか、画像をファイルキャッシュしておいてオフラインでも表示するとか、複数の異なる形式のWebAPIをプロキシして同じ形で扱えるようにしたり、ということも簡単にできる。

また、逆に「特定のリクエストに限って特別な処理をする」ではなく「特定のリクエストはスルーするけど他はブロックする」のような使い方もできる。最近(というには結構前からだけど)話題になったのは、iPhoneのWebViewにこういう脆弱性がある、という話。

何故デバイスのアドレス帳や着信履歴みたいなのの生のデータにJSからアクセスできる必要があるのか良く分からないし、おかしな仕様だと思うのでバージョンアップで塞がれるだろうとは思うけど、少なくともiOS5.0まではこの問題は残っているので、「アプリ内でUIWebViewにloadHTMLStringさせて、外部から取得したHTMLをfile://等のスキームで表示させている」ような場合には対処する必要がある。簡単な対策としてはきちんとbaseURLを設定しておけばいいのだけど、そうすると今度はCSS、JS、画像などのバンドル内部に持っているリソースが使えなくなる。「外部から取得したHTMLをアプリ内のUIWebViewで表示したいけど、アプリ内の安全なリソースにはアクセスさせたい」場合に、NSURLProtocolで通信をフックする仕組みが使える。

前者の記事だと、NSURLProtocolのサブクラスでバンドル内のファイルを開いて、そのデータを返してしまう、というやり方をしている。また、後者の記事やPhoneGapの実装では、NSURLProtocolの「登録時の逆順に対応できるかどうかチェックする」「YESを返せばそこで止まり、NOを返すと次のNSURLProtocolのチェックに進む」という性質を利用して、「ホワイトリストに載ってないリクエストの場合は、canInitWithRequest:でYESを返した上でエラーなり空レスポンスなりを返し、ホワイトリストに載っている場合はNOを返して通常のhttpやfileプロトコルとして処理させる」という方法でサンドボックス外のリソースにアクセスさせないようにしている。

まとめ

  • NSURLProtocolを使えばさくっと独自スキームを定義できるよ
  • WebViewみたいに細かく制御しづらいものの内部の通信もフックできるよ
  • セキュリティ面でも意味があるよ

という感じなので、是非試してみてください。

前の記事で予告した通り、今度はNSRegularExpressionの話。

正規表現でマッチした部分文字列を取得する

まずNSRegularExpressionオブジェクトを作って、それのメソッドにNSStringのオブジェクトを渡す、という形で使う。まぁ説明するよりコード見た方が早い。

NSString *string = @"「そんな正規表現で大丈夫か?」「大丈夫だ、問題ない」";
NSError *error   = nil;
NSRegularExpression *regexp =
  [NSRegularExpression regularExpressionWithPattern:@"「そんな(.+)で大丈夫か?」「(.+)」"
                                            options:0
                                              error:&error];
if (error != nil) {
  NSLog(@"%@", error);
} else {
  NSTextCheckingResult *match =
    [regexp firstMatchInString:string options:0 range:NSMakeRange(0, string.length)];
  NSLog(@"%d", match.numberOfRanges); // 3のはず
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:0]]); // マッチした文字列全部
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:1]]); // "正規表現"
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:2]]); // "大丈夫だ、問題ない"
}

地味にややこしい。Rubyで書いたらこんなんで済むのに。

# coding: utf-8
if "「そんな正規表現で大丈夫か?」「大丈夫だ、問題ない」" =~ /「そんな(.+)で大丈夫か?」「(.+)」/
  puts $&
  puts $1
  puts $2
end

まぁRubyやPerlと比べるのは(少なくとも文字列操作や正規表現に関して言えば)フェアじゃないですけど!とにかくこれで正規表現で部分文字列を探せるようになりました、と。

ちなみに、-firstMatchInString:options:range:というメソッド名で分かると思うけど、これは最初にマッチした箇所しか取ってこない。マッチした箇所全て欲しければ、-matchesInString:options:range:を使えば、NSTextCheckingResultが入ったNSArrayが返ってくる。別に返り値はずっと取っておく必要はなくて、単にマッチする毎になんか処理をしたいんだよ、ってときは、-enumerateMatchesInString:options:range:usingBlock:が使える。さっきの-firstMatchInString:options:range:を書き換えるとこんな感じになる。

NSRegularExpressionOptions options = 0;
NSRange range = NSMakeRange(0, string.length);
id block = ^(NSTextCheckingResult *match, NSMatchingFlags flag, BOOL *stop){
  NSLog(@"%d", match.numberOfRanges);
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:0]]);
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:1]]);
  NSLog(@"%@", [string substringWithRange:[match rangeAtIndex:2]]);
};
[regexp enumerateMatchesInString:string options:options range:range usingBlock:block];

Blocksの使い方は以前書いた記事とか読んでもらえると分かるかもしれない。あの記事を書いた時点ではiOS4.0を想定してアプリ作れなかったので実質まともに使えるのがSnow Leopardだけだったのだけど、今ならiPhone/iPadともに4系前提で作れるし、そもそもNSRegularExpression自体がiOS4.0以降にしか無いのでNSRegularExpressionを使える環境ならBlockも使えるので問題ない。

置換する

正規表現が使えるなら一番やりたいのは置換だろう、ということでもちろん置換もできる。-stringByReplacingMatchesInString:options:range:withTemplate:というのがそれ。

  NSString *string = @"「そんな正規表現で大丈夫か?」「大丈夫だ、問題ない」";
  NSString *template =
    @"$0\n→($2砕け散る)\n→「神は言っている、ここで死ぬ運命ではないと」\n→「$1」「一番いいのを頼む」";
  NSRegularExpression *regexp =
    [NSRegularExpression regularExpressionWithPattern:@"「(そんな(.+)で大丈夫か?)」「.+」"
                                              options:0
                                                error:nil];
  NSString *replaced =
    [regexp stringByReplacingMatchesInString:string
                                     options:0
                                       range:NSMakeRange(0,string.length)
                                withTemplate:template];
  NSLog(@"%@",replaced);

最初は話を聞かなかったあいつもちゃんと一番いいのを頼んできたので、今度は大丈夫だろう。しれっと$0とか$1とか使ってるけど、もちろんちゃんと置換文字列の中でキャプチャした部分文字列を参照したりできてるはず。

ただ、-stringByReplacingMatchesInString:options:range:withTemplate:は文字列そのものを置換してるわけじゃなくて、引数のNSStringのオブジェクトをcopyして置換したものを返してくる。なので、元のstringは何も変わってないので変わったつもりで使おうとしたらアレ?ってなるし、毎回文字列のコピーをするので場合によっては無駄になる。その場合は-replaceMatchesInString:options:range:withTemplate:の方を使う。基本的には-stringByReplacingMatchesInString:options:range:withTemplate:と同じなんだけど、以下の点が違う。

  • 引数にNSStringでは無くNSMutableStringを取る
  • 引数のオブジェクトのコピーではなく引数のオブジェクト自体を置換する
  • 返り値は置換後の文字列ではなく整数値で、置換箇所の数を返す

というわけで、ある正規表現で置換した文字列をさらに別な正規表現で置換して、みたいなことをやる場合はこっちのメソッドを使うべき。

ちなみに、上記二つのメソッドはマッチした箇所を全部置換する。例えば下のようなコードだと「大丈夫か」と「大丈夫だ、」が両方置換される。

  NSString *string = @"「そんな正規表現で大丈夫か?」「大丈夫だ、問題ない」";
  NSString *template =
    @"チョ☆チョニッシーナ☆まっソコぶれっシュ☆エスボグリバンバーベーコンさんだね!";
  NSRegularExpression *regexp =
    [NSRegularExpression regularExpressionWithPattern:@"大丈夫(か|だ)、?"
                                              options:0
                                                error:nil];
  NSString *replaced =
    [regexp stringByReplacingMatchesInString:string
                                     options:0
                                       range:NSMakeRange(0,string.length)
                                withTemplate:template];
  NSLog(@"%@",replaced);

もしマッチした箇所の内特定の部分だけを置換したい場合は、-firstMatchInString:options:range:とか-matchesInString:options:range:でNSTextCheckingResultのオブジェクトを取得しておいてから、-replacementStringForResult:inString:offset:template:を使う、みたいな感じになるかしら。ちょっと面倒な気もするけど。

RegexKitLite or NSRegularExpression

両方書いてみた感想で言うと、個人的にはRegexKitLiteのNSStringにメソッド生やしてくアプローチのAPIのが使い易いと思った。CoreFoundation使ってごりごり書いてるのでパフォーマンスも悪くないし、割と早い段階からBlocksに対応してたりとアクティブに開発されてるし、その気になればソース読めるし(まぁ、チラ見しては見たもののあんまり読む気にはならないのだけども)…とか考えると、既にRegexKitLiteを使ってるなら別に無理にNSRegularExpressionに乗り換える必要は無い気がしてくる。iOS4.0以前のバージョンもターゲットにするなら他に選択肢はないし、あと何故かNSRegularExpressionクラスはiOSにしか無くてMacOSXでは使えないという面白いことになってるので、iOSでもMacでも動くようなコードを書く場合もやっぱりNSRegularExpressionは使えない。

とは言えNSRegularExpressionの方はFoundationの一部なので、数カ所正規表現での置換を使いたいが為に外部のコード落としてきてプロジェクトに組み込んでlibicucoreに忘れずにリンクして…ってしないで済むなぁとか、万が一iOSの内部の実装が変わったりなんかの規約が変わったりしてもおそらく書き換えないで済むだろうなぁという多少の安心感とかはある。ので、これから作るアプリで、4.0以降のみをターゲットにしてる場合は、NSRegularExpressionを使って書こうかなぁなんて思ったりしてたり。

ちょっと前に書こうと思ってて忘れてたネタ。iOSアプリ内で正規表現を使ってごにょごにょしようと思ったらRegexKitLiteを導入するのが一番てっとりばやいのだけど、iOS 3.2以降はFoundation Framework内でも地味に正規表現が使えるようになってきてるのでメモがてら記事にしておく。

NSRegularExpressionSearch

Cocoaで文字列中に別な文字列が含まれているかどうかを知りたいときは、NSStringの-rangeOfString:というメソッドを使う。RubyのString#indexみたいな感じで、見付かった文字列がどこにあるかの位置を返してくれる。こんな感じ。

NSString *string = @"I love Udon.";
NSRange match = [string rangeOfString:@"Udon"];
if (match.location != NSNotFound) {
  NSLog(@"Found: %@",[string substringWithRange:match]);
} else {
  NSLog(@"Not Found");
}

これにもう少し細かく色々なオプションを指定できる-rangeOfString:options:というメソッドがあるのだけど、iOS3.2以上のバージョンだとこのオプションにNSRegularExpressionSearchというのが指定できるようになっている。実際に使うときはこう。

NSString *string = @"1日3食のうち4食はうどんを食べたいと思っている。";
NSRange match = [string rangeOfString:@"[0-9]+食" options:NSRegularExpressionSearch];
if (match.location != NSNotFound) {
  NSLog(@"Found: %@",[string substringWithRange:match]);
} else {
  NSLog(@"Not Found");
}

rangeOfString:に正規表現(の文字列)を渡せるようになってちょっと便利。書式はICU-comaptibleだそうだけど、RegexKitLiteもlibicucoreを使ってるので、RegexKitLiteを使ってた人は得に気にすることなく使えると思う。

これだけでも大分マシにはなったんだけど、さっきのサンプルコード見て分かる通り最初にマッチした部分しか取ってこれないし、もしかして-stringByReplacingOccurrencesOfString:withString:options:range:とかにも正規表現使えるのかなとwktkしたのだけど、「You can use this option only with the rangeOfString:... methods.」だそうで。マッチした箇所を全部取ってくるとか置換するとかは別な方法でやるようだ。

NSRegularExpression

さっきのはNSStringの文字列検索のオプションだったけど、正規表現そのものを扱うNSRegularExpressionというクラスがある。NSRegularExpressionSearchオプションは3.2以降であれば使えるけど、NSRegularExpressionクラスは4.0以降。つまり今までiPadでは使えなかったので、RegexKitLiteを置き換えるには至らなかった。

が。そろそろiPad版を含むiOS4.2がリリースされるので、ようやくiPadでも4系の機能が使えるようになるのです。弟の仇をトルノデス。ということで次回はNSRegularExpressionを使った文字列検索を記事にします。予告。

どうも。「いらないって言ってたのに開発機をいじってたら欲しくなっちゃいましたの法則」が発動してiPadも先週結局買っちゃった僕です。

iPad買ったら当然JailBreakしてターミナルでコード書きまくってやる、と思ってたんだけど、いきなりmobileterminalが動かなくて躓く。ふぁっきん。とは言え自分で修正するとか新しく作るとかまでする気も起きず、あーそうだJSとかHTMLくらいだったらiPad上で書いたりデバッグしたりできるエディタあったよねーと思って、AppStoreで探してみたらちらほら見付かるも、コレ、というのはまだ無い様子。当たり前だけど、Syntax Highlightができるメモ帳程度なんだよなー。

エディタのViewを作る

ここで「タッチインターフェースを有効活用したコードエディタ」ってのを思い付いたら一時代築けそうなんだけど、別に今のところなんかアイディアがあるわけでもないので、誰かが作ればいいなー、もしくはアイディアくれるといいなーとか言うだけ言っておいて、とりあえずごく普通のテキスト入力画面の話をする。

例えば

  • 画面いっぱいにTextViewが表示されてて
  • Viewが表示されたらTextViewにフォーカスが当たるようにして
  • キーボードが表示されたらそれに合わせてViewをリサイズもしくはスライドさせて
  • キーボードが非表示になったらまたViewをリサイズもしくはスライドさせる

ようなごく普通のテキスト入力画面を作りたいとする。

TextViewにフォーカスを当てるのは簡単で、例えばViewControllerのviewDidAppearとかの中でTextViewのresignFirstResponderメソッドを呼んでやればいい。そうするとTextViewにフォーカスが当たってキーボードがせりあがってくる。キーボードのサイズは、UIKeyboard(Will|Did)(Show|Hide)Notificationていう名前でキーボードの表示、非表示の際に通知が飛ぶので、それをViewControllerで受けてViewをいじってやればいい(この記事とかが参考になった: 画面いっぱいのUITextViewがキーボードに隠れないようにする ? LANCARD.LAB|ランカードコムのスタッフブログ)。リンクの記事だとアニメーションさせてるけど、UIKeyboardDidShowNotificationを受けて単にself.view.frameの値を変えてやるだけでもそれっぽくなる。

あれ、Viewが消えた

iPhone/iPod Touchだとこれで上手く行くんだけど、実はiPadだとこのままだと上手く行かない。どうなるかというと、「日本語キーボードと英語キーボードを切り換えるとViewがどんどん小さくなる、消える」みたいなことになる。これ何でなのかなと思ったら、iPadだと日本語キーボードのときはキーボードの上に変換候補を表示するバーが出るのでその分だけ英語キーボードと表示領域のサイズが違うんだけど、表示領域のサイズが変わる度にUIKeyboard(Will|Did)ShowNotificationが通知される。前述のコードだとkeyboardWillShow:が呼ばれる度にキーボードの高さの分だけViewの高さを縮めてるので、連続でkeyboardWillShow:が呼ばれちゃうとどんどん小さくなってしまうという話。

これを防ぐには、単にself.view.frame.size.heightからキーボードの高さ分丸ごと引くんじゃなくて、UIKeyboardFrameBeginUserInfoKeyでキーボードの表示領域変更前の、UIKeyboardFrameEndUserInfoKeyで変更後のサイズが取れるので、その差分を取ってViewのサイズを調整してやる必要がある。例えばこんな感じ。

- (void)keyboardWillShow:(NSNotification *)notification {
  NSDictionary *userInfo = [notification userInfo];
  UIView *superview      = self.view.superview;
  CGRect beginRect       = [superview convertRect:[[userInfo objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue] fromView:nil];
  CGRect endRect         = [superview convertRect:[[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue] fromView:nil];
  CGRect superviewFrame  = superview.frame;
  CGRect viewFrame       = self.view.frame;

  viewFrame.size.height -=
    (beginRect.origin.y > superviewFrame.size.height) // キーボードが表示されてないときは origin.y が全体の表示領域より下になってる
    ? endRect.size.height
    : endRect.size.height - beginRect.size.height;

  // 他にもやることがあればごにょごにょ

  self.view.frame = viewFrame;
}

hideの方は連続で呼ばれることはないのでそのままでも使えるけど、これも「既に非表示になってたら処理をスキップする」とかちゃんと入れといた方がいいです。あと、iPhoneSDK 3.2からUITextFieldやUITextViewにカスタムキーボードを付けてあげることができるようになったけど、iPhoneではまずやらないと思うけどiPadでは「それぞれの入力欄にそれぞれのカスタムキーボードを設定してあり、しかも全部形が違う」みたいなこともやろうと思えばできる。そうなるとコードのカオス度が格段に上がるし、上記のコードだとまた残念なことになるのでご注意を。

Objective-Cで、参照先のオブジェクトから参照元に通知を送る方法。」を読んてで思ったんですが、若干冗長な気がする。

プロトコルを使う

プロトコルを使うんだったら、最初からそのプロトコルに適合するオブジェクトしかdelegateになれないようにしちゃった方がいいと思う。こんな感じで。

@interface Foo : NSObject {
  id<SampleDelegate> delegate;
}
@property (assign) id<SampleDelegate> delegate;
@end

こうしちゃえば、そもそもSampleDelegateに適合しないオブジェクトはdelegateになれないので、conformsToProtocolのチェックは必要なくなる。UIKitでも(例えばUITableViewのdelegateとか)大体そうなってますね。あと、ObjCはレシーバがnilの場合はメッセージ送信を単に無視するので、respondToSelectorとか送ってもYESが返らないのでこの場合はnitチェックもしなくていいと思う。だから、notifyObjectChangedの定義はこれで十分。

- (void)notifyObjectChanged {
  if ([self.delegate respondsToSelector:@selector(objectChanged:)]) {
    [self.delegate objectChanged:self];
  }
}

非形式プロトコル(カテゴリ)を使う

プロトコルがoptionalなメソッドしか規定してなくて、あるメソッドを定義したデリゲートがある場合はそれを呼ぶけど、そうでない場合は無視するかデフォルトの動作をする、っていうような場合は、そもそもプロトコルで規定しないでカテゴリで実現しちゃう手もある。

@interface NSObject (SampleDelegate)
- (void)objectChanged:(id)object;
@end

@interface Foo : NSObject {
  id delegate;
}
@property (assign) id delegate;
@end

ちなみにnotifyObjectChangedの実装は同じでいい。このタイプの例はNSURLConnectionとか。

こっちの利点はプロトコルの宣言がいらないので、既存のクラスや外部のライブラリのクラスのインスタンスでもdelegateにできること。それが何であるかはどうだっていいんだ、ただ送ったメッセージに答えてくれる何かでありさえすれば、みたいなときにはこの方法でもいい。

欠点は、プロトコルと違ってコンパイル時にdelegateが期待してるオブジェクトかどうかを解決できないこと。元記事の例みたいにoptionalなメソッドしか規定してない場合だと、プロトコルに適合してるからと言って期待してるオブジェクトかどうかははっきりしないので大差ないんだけど、仮にそうであってもプロトコル宣言を強制することによってデリゲートになり得るクラスを限定するってこともあるので、この辺はケースバイケース。

Key-Value Observingを使う

あと、「あるオブジェクトのあるプロパティが変更されたことを知りたい」っていう用途に限って言えば、Key-Value Observingを使う手もある。

@implementation Bar

- (void)test {
  Foo *f = [[Foo alloc] init];
  [f addObserver:self
      forKeyPath:@"hoge"
         options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld)
         context:NULL];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    // do something
}

@end

こういう風にしておくと、fのhogeプロパティの値が変更されたときに、BarのインスタンスのobserveValueForKeyPath:ofObject:change:context:メソッドが呼ばれる。その際、keyPathには@"hoge"が、objectにはfが、changeには{ old: /* 変更前の値 */, new: /* 変更後の値 */ }というDictionaryが、contextにはNULLが渡ってくる。

これの利点は、「Fooクラス側には全く手を加えなくていい」「あらゆるオブジェクトに対して、一貫したインターフェースで同じように設定できる」ということ。Cocoaのクラスだろうが外部のライブラリのクラスだろうが自分で作ったクラスだろうが同じように「あるプロパティの値が変更されたら教えてねー」っていう設定ができるし、参照先のオブジェクトが参照元のオブジェクトのことを気にする必要が無くなって関係性が緩くできる。Barの仕様を変更したくなってもFooをいじる必要はない。あと、delegateの場合と違って通知を受けとるオブジェクトが複数設定できるので、アプリケーションのあちこちが同時多発的に状態変化するみたいなことができる。

欠点は、「f.hogeに代入するか、[f setValue:obj forKey:@"hoge"]が呼ばれたときだけしか通知されない」ということ。なのでFooクラスの内部でインスタンス変数を直接弄って内部状態が変わったりしたときには通知されない。あとは、あらゆるオブジェクトに対して使えるので便利なんだけど、どのオブジェクトからの通知も必ずobserveValueForKeyPath:ofObject:change:context:を呼ぶので、沢山通知を設定するとobserveValueForKeyPath:ofObject:change:context:の中身が大分カオスなことになるので、ご利用は計画的に。

他には

NSNotificationを使う方法もあるけど、多用すると処理がどこでどうなるかわかりづらくなるので割と慎重に使った方がいいかも。まぁこれはKey-Value Observingでもそうなんだけど、NSNotificationの方がより汎用性が高いのでよりこんがらがり易くてよくハマる。

scrollView.canCancelContentTouches = NO;
[scrollView setCanCancelContentTouches:NO];

って書き方があってこれみんなどういう使い分けしてるんだろうなぁ。。って思ってます。

セッター - poohtarouの日記

セッターというか、ドット記法の話かな。

ドット記法と普通のメソッドの使い分け

まぁ、まずはこんなクラスがあったとします。

@interface Book : NSObject {
  NSObject *title;
}
@property (retain) NSString *title;
@end

このクラスのオブジェクトを作ってtitleを設定/参照するコードはこんな感じです。

Book *book = [[Book alloc] init];
// ドット記法
book.title = @"Dynamic Objective-C";
NSLog(@"%@", book.title);
// メソッド
[book setTitle:@"詳解 Objective-C 2.0"];
NSLog(@"%@", [book title]);

これは上と下どっちのコードも同義。ドット記法でアクセスすると、setTitle:メソッドやtitleメソッドが呼ばれる。getter/setterを自動生成させないで自分で実装した場合もちゃんとそのメソッドを呼んでくれる。じゃあこんな場合はどうか。

id book = [[Book alloc] init];
book.title = @"Dynamic Objective-C";
NSLog(@"%@", book.title);

これはコンパイルエラーになる。id型のオブジェクトのメンバにドット記法でアクセスしようとすると怒られる。いやid型じゃなくてちゃんとクラスを明示して変数宣言すればいいじゃんて?NSArrayのobjectAtIndex:やNSDictionaryのobjectForKey:は返り値の型がid型ですよね。キャストすればいい?だって中に入ってるのがBookクラスのオブジェクトかどうかわかんないじゃん。isKindOfClass:で調べてからキャストする?いやいや、そのArrayの中にはGameクラスのオブジェクトも一緒に入ってて、GameクラスもsetTitleできるからそこは区別なく扱えた方が便利なんだよ!…みたいなこともあるわけで。そんなときはこう書くかなぁ。

id obj = [array objectAtIndex:0]; // Bookのオブジェクトが入ってると期待できるとする
if ([obj respondToSelector:@selector(setTitle:)]) {
  [obj performSelector:@selector(setTitle:) withObject:@"化物語 上"];
}

はいできた。これでBookクラスのオブジェクトをid型の変数で受けてもちゃんとsetTitle:できました。ついでにsetTitle:メソッドを実装してるオブジェクトであれば、Bookクラスと継承関係になくてもsetTitle以外は全然無関係のインターフェースを実装してようとも同じように扱えます。アヒルのように鳴くものはアヒルではなく隣のおばあちゃんでした。じゃなかった、アヒルのように鳴くものはアヒルです。ちなみに、上のコードは実は

id obj = [array objectAtIndex:0];
if ([obj respondToSelector:@selector(setTitle:)]) {
  [obj setTitle:@"本当は怖いグリム童話"];
}

って書いてもコンパイル通るし実行時にエラーも出ない。まぁ警告出るけどね。んで、こういう場合はプロトコルを定義してあれば(そしてobjがそのプロトコルに適合してれば)警告が出ない。

// こんなプロトコルがあるとする
@protocol Title <NSObject>
- (NSString *)title;
- (void)setTitle;
@end
...
id <Title> obj;
...
[obj setTitle:@"本当は怖いグリム童話"];

簡単にまとめると、明示的にクラス名を指定して変数を宣言してるときはドット記法で、id型で受けるべき時はメソッドでっていうのが一応の使い分けかなー。元記事の例の場合はscrollViewはUIScrollView型で変数宣言してて、そのコンテキストではUIScrollViewであることがはっきりしてるのでドット記法でいいと思う。具体的なクラスを想定しているわけではなくて、あるアクセサを持っている何かのオブジェクトっていう扱いをするときは、respondToSelector:とperformSelector:を使うか、プロトコルを定義しといてメソッド呼び出しするか、って感じになると思う。

Key Value Conding

ところで、ObjCにはドット記法とメソッド呼び出しの他にもう一つ、オブジェクトのメンバにアクセスする方法がある。しかもこれはプロパティとドット記法が導入されたObjective-C 2.0になる前からある。例えばさっきのBookクラスのオブジェクトに対してだったらこんなことができる。

[book setValue:@"イチャイチャパラダイス" forKey:@"title"];
NSLog(@"%@", [book valueForKey:@"title"]);

恐ろしいですね。まるでオブジェクトがDictionaryか何かのよう。これはNSObjectの子孫にあたる全てのクラスで使える。そしてこれの恐しいのは、アクセサメソッドやプロパティが宣言されてなくても使えてしまうということ。例えばvalueForKey:@"title"だったら、

  1. titleメソッドが定義されてたらそれを呼ぶ
  2. titleメソッドが無くて、getTitleメソッドが定義されてたらそれを呼ぶ
  3. どっちも無くて、インスタンス変数titleが宣言されてたらそれを返す
  4. どれも無くて、インスタンス変数_titleが宣言されてたらそれを返す

っていう順番で解決されるので、Bookクラスの場合titleをプロパティとして宣言してる、つまりtitleメソッドが定義されてるのでそれが呼ばれる。もしプロパティとして宣言してなくても、title変数があるので3番目でひっかかってそれが返る。ちなみにインスタンス変数が@privateで宣言されてても関係ない。便利なんだけど怖い。1、2はともかく3、4は大分アレげなので、内部でしか使わない意図せず変更して欲しくないインスタンス変数は@privateで宣言した上でname_とかして上のルールにひっかからないようにしておく癖を付けたほうがいいと思う。外部からアクセスしてもいいやつでも名前は変えた上で明示的にプロパティで宣言した方が安全かなー。

元記事の話とは直接関係無いけど余談でした。なんでそんな仕組みがあるのってあたりは気になる人は「Key Value Coding」とかで調べてみるといいと思う。これはこれで便利だし、AppKitとかUIKitとかでは結構使われてるので。

追記

そうだブコメで指摘をもらってたんで追記しようと思ってたのすっかり忘れてた。iPhoneアプリでKey-Value Codingを一番使うであろう場面はCoreDataのNSManagedObjectですね。CoreDataでエンティティを定義してデータをつっこんだり取得したりするときは、NSManagedObjectかそれを継承したクラスがモデルオブジェクトになる。サブクラスを作るときはプロパティを定義しとけばいいんだけど、NSManagedObjectにはもちろん中のデータへのアクセサは定義されてない。じゃあどうするかと言うと、valueForKey:とsetValue:forKey:で値の取得や変更をするってことになる。

あと、これの次の記事で書いたけど、Key-Value Observingって仕組みがとても便利なので、プロパティについて調べたらついでにKey-Value CodingとKey-Value Observingについて調べておくといいと思います。まる。

詳解 Objective-C 2.0詳解 Objective-C 2.0
著者:荻原 剛志
販売元:ソフトバンククリエイティブ
発売日:2008-05-28
おすすめ度:4.0
クチコミを見る
Dynamic Objective-CDynamic Objective-C
著者:木下 誠
販売元:ビー・エヌ・エヌ新社
発売日:2009-03-27
おすすめ度:4.5
クチコミを見る

Rubyと比べながらBlocksをいじってみたりBlocksでNSArrayにmapメソッドを生やしてみたりしてきたので、そろそろGrand Central Dispatch(GCD)も試してみる。あんま関係ないけど、グランド・セントラル・ディスパッチってなんか必殺技っぽいよね。じゃあ一緒に高らかに叫んでみようか。せーの、グランド!セントラル!ディスパッチ!!

GCDってなにさ

ドキュメント嫁。

…だけだと流石に不親切なので、一応簡単に説明すると、APIを通してぽんぽん処理をqueueにつっこんでってやると、ランタイムの方でそれを上手いこと並列実行しといてやるよ!安心しろチェリーボーイ共、スレッドのことは俺が面倒見てやるぜ!って仕組み。そんな口調なのかどうかはわかんないけど、まぁ大体そんな感じ(適当)。

例によってRubyと比較

まぁ、こんなコードがあったとします。

# ruby
f = lambda {
  puts "0.25秒後から本気出す"
  sleep 0.25
}
t = Time.now
20.times do
  f.call
end
p Time.now - t

んで、それを例によってObjCでBlocksを使って書くとこんなコードになります。ちなみにどっちも別にBlocksやlambdaでやる必要は無いんだけど、この後のコードと比較の為にわざとそうしてるのでスルーしておくれやす。

# ObjC
void (^f)(void) = ^{
  NSLog(@"あと0.25秒だけ寝させてー");
  [NSThread sleepForTimeInterval:0.25];
};
NSDate d = [NSDate date];
unsigned int i = 20;
while (i--) {
  f();
}
NSLog(@"%f", [[NSDate date] timeIntervalSinceDate:d]);

まぁ見ての通り、標準出力に一言言ってから0.25秒sleepするだけの簡単なお仕事を20回ほどやってもらってます。あたり前のことだけど、0.25*20で5秒+αくらいの時間がかかるし、0.25秒っつってんのに5秒待たせるとか相当いい加減なやつだ。

んで、こんな風に、それぞれの処理が独立してるけど一個一個は結構時間かかる、みたいなのは、並列に実行させちゃったらいいんじゃね、みたいなことを偉い人は言いました。

# ruby
f = lambda {
  puts "0.25秒後から本気出す"
  sleep 0.25
}
t = Time.now
20.times do
  Thread.new { f.call }
end
(ThreadGroup::Default.list - [Thread.current]).each{|th|th.join}
p Time.now - t

まだ若干怠けてるけど、まぁ0.3秒行かない程度で終わる。5秒に比べたら一瞬みたいなもんだよね。

じゃあ次はそれをObjCで…と言いたいところなんだけど、ObjCで上のRubyのコードをNSThreadってクラスを使って書こうとすると、割と面倒い。特定のコンテキストを別スレッドで実行しようと思うと、detachするのにtargetとselector、つまりスレッドで実行されるオブジェクトとそいつから呼び出されるメソッドがなくちゃいけない。んでもって、作ったスレッドを自分で管理しなきゃいけない。一応適当なサンプルは書いたけど、あんまりこれをObjCで自前で書くことは無いと思う(理由は後述する)。

さて、GCDです

最初の方に言ったけど、大分ざっくり言うとGCDってのは「処理のブロックをキューにつっこんでってやると裏で上手いこと並列に処理してくれる」ものです。要は並列処理のめんどい感じを多少楽にしてくれるのがGCDの兄貴だってことです。兄貴なのか姉貴なのかは知りませんが。どっちかというと僕はお姉さんが好きですがどうでもいいことです。

# ObjC
dispatch_block_t block = ^{
  NSLog(@"あと0.25秒だけ寝させてー");
  [NSThread sleepForTimeInterval:0.25];
//  NSLog(@"%@", [NSThread currentThread]);
};
//NSLog(@"%@", [NSThread currentThread]);

NSDate d = [NSDate date];

// ここからGCD登場
dispacth_group_t group = disptach_group_create();
disptach_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
unsigned int i = 20;
while (i--) {
  disptch_group_async(group, queue, block);
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
// ここまでGCDのお仕事

NSLog(@"%f", [[NSDate date] timeIntervalSinceDate:d]);

はい。Rubyの方をThreadを使って書き直したときと同じく、0.3秒行かないくらいの時間でさくっと処理してくれました。コメントアウトしてるのを戻せば、ちゃんと別々のスレッドで動いてるのも確認できると思います。ちなみにObjCの文法で書いてるとこを削れば普通にCでも使えます。使いたいときは#include <dispatch/dispatch.h>してください。

変わったところを解説すると、まずブロックの型がdispatch_block_tに変わってる。これは後で出てくるdispatch_group_asyncの引数の型なんだけど、void (^)(void)、つまり何も取らず何も返さないブロックって定義になってるので実はさっきと何も変わってない。

次にdisptach_group_createをしてgroupを作ってるけど、これはまぁ名前通り非同期に実行する処理をグルーピングするためのもの。Rubyの方でもThreadGroupが出てきたけど、あれと一緒で最後のdispatch_group_waitで一連の処理が全部終了するまで待ってやる為に使う。今回は全部の処理が終わるまでの時間が見たかったのと、アプリケーションとかじゃない普通のCUIのコマンドとして作ったときに何も考えずに非同期処理させるとdispatchしたのが終わる前にmainが終わっちゃうのでwaitする必要があったけど、GUIアプリケーションとかデーモンとかだとその心配はないのでグルーピングせずに単にdispatch_asyncしちゃってもいい。

んでここからが本質、dispatch_get_global_queueとdispatch_async(またはdispatch_group_async)。といっても別に大したことではなくて、

  1. queueを用意します
  2. dispatch_asyncにqueueとblockを渡します
  3. あとは裏でよしなにやってくれます

以上。中では「システムの負荷を見てスレッドを作るか待つか決める」「スレッドが一個も空いてなければ作るけど、さぼってるやつがいたら再利用する」「あっちこっちから放り込まれたブロックをどのスレッドに割り当てたら効率良いか考える」とか色々やってんだけど、使う側としてはそんなこと気にする必要無いし、それどころかスレッドが作られてることすら隠蔽されてる。やったのは単に関数にブロックを渡しただけ。ゆとりの僕でもできる簡単なお仕事です。ちなみにdispatchしてwaitするあたりの処理を続けて何回も実行すると、ちゃんとスレッド再利用してるのが確認できる。

他にも、globalって名前が付いた関数があるからにはglobalじゃないqueueを作る関数もあるとか、asyncって名前が付いた関数があるからにはsyncして実行する関数もあるとか、メインスレッドで動作するqueueがあるとか、さっきのコードではわざわざforループ回したけどループにはループ専用のdispatch_applyがあるとか、まぁ色々あるんだけど、Xcodeのあのアホみたいに使い辛いドキュメントビューワの検索窓にdispatch_って入れてやるといっぱい出てくるので見てみてくれればいいかと思います。

でもそれCじゃん

ええ。ここまでは誰がどうみてもCの関数、っていうかさっきも書いた通り実際<dispatch/dispatch.h>をincludeすればCでも使えるAPIです。いやさ、確かにObjCの文法はキモいけどさ、せっかくObjCで書いてるのにCの関数使うってどうなのよって?ご心配なく。ObjCならObjC流に実装する方法はもちろんある。

「並列実行」「どんどんキューにつっこむ」「スレッドの面倒はキューが見てくれる」あたりで、LeopardまでのOSXとかiPhoneSDKとかでアプリを書いたことある人は「それNSOperationとNSOperationQueueでできるじゃん」と思ったはず。これもキューにオペレーションオブジェクトをどんどんつっこんで行けば非同期でよしなにやってくれるクラスで、それ自体はGCDやBlocksとは関係なく前から使える。ので、もともとあんまり自前でNSThreadを作ったり管理したりはやったことなかった。じゃあObjCだと何も変わらないじゃんと思ったけど、Blocksが導入されたことでNSArrayやNSDictionary同様便利な機能が追加されているという。

# ObjC
void (^block)(void) = ^{
  NSLog(@"あと0.25秒だけ寝させてー");
  [NSThread sleepForTimeInterval:0.25];
//  NSLog(@"%@", [NSThread currentThread]);
};

//NSLog(@"%@", [NSThread currentThread]);
NSDate d = [NSDate date];

// こっからNSOpera(ry
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
unsigned int n = 20;
while (n--) {
  [queue addOperationWithBlock:block];
}
[qeueu waitUntilAllOperationsAreFinished];
// ここまでNSOpera(ry

NSLog(@"%f", [[NSDate date] timeIntervalSinceDate:d]);

おお、これは便利だな。Blocsを使わない場合は事前にNSOperationクラスを継承して独自のOperationクラスを作っとくとか、NSInvocationOperationを使うとかしてたところを、-[NSOperationQueue addOperationWithBlock:]を使えばブロックを渡してやるだけで非同期実行できちゃう。ブロックをひとまとめにしてオペレーションにしてくれるNSBlockOperationってクラスや、オペレーションが終了した後に実行される処理をブロックで設定できる-[NSOperation setCompletionBlock:]ってメソッドが追加されてて、「処理を一括りにして並列実行する」コードが大分書きやすくなっている。

MacRuby

RubyとObjCの話をしてるのにMacRubyさんを完全にスルーするという素敵なプレイをやってのけてきたわけですが、実はMacRubyさんはとっくにGCDに対応してやがります(MacRuby » An Introduction to GCD with MacRuby)。凄いな、ほんと、どこまで行くんだろう。

ついでに

Blocks入門NSArrayにmap生やしたのとこの記事のコードをのサンプルはgistに上げてみたので、まぁ一応一通り動く例になってるはず。あーあと簡易ベンチマーククラスみたいのも作ってみたので入れといた。適当なので実用するのはおすすめしません。Snow Leopardでrubyとrakeが入ってる環境なら、cloneしてきてrakeすればコンパイルできるはず。

参考記事

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で遊んでみた編。多分。

書こう書こうと思ってたけど忘れてたのを、PerlエンジニアのためのObjective-C Blocks入門を見て思い出した。すいませんタイトルは便乗です。

試しに書いてみる

Blocksってのが何者なのかはさっきの記事なりAppleのドキュメントなりを見てもらえばいいと思うんですが、まぁウケが良さそうなので付けたタイトルにのっとってRubyと比較してみる。

f = lambda {|x|
  puts "#{x}のこと以外は何も考えられない"
}
f.call("うどん")
void (^f)(id) = ^(id x) {
  NSLog(@"世界の全てを敵に回しても、僕は%@の味方だ", x);
};
f(@"うどん");

なんだ、そっくりじゃない。似てる似てる。

上がRuby版、下がObjC版。下は普通引数にはNSString*とかを使うと思うけど、まぁRuby版と挙動を合わせるためにidにしてみた。それぞれコピペして動かしてみるといいよ。好きな子の名前とか入れてみるといいよ。

なに?ブロックを変数に入れてるとこの宣言がキモい?じゃあこうだ。こうすればいい。

id f = ^(id x) {
  NSLog(@"世界の全てを敵に回しても、僕は%@の味方だ", x);
};

id型の変数に代入できてしまった。実はこのブロックはオブジェクトなので、普通にid型として扱えるし、メッセージのレシーバになったりできる。もちろんid型の変数に入ってるときは関数みたいに使ったりできないので、実際使うときは

((void (^)(id))f)(@"うどん");

とかキャストしてやらなきゃいけないわけで、id型で宣言する意味はあんまりない。ただ、id型として扱えるってことは、NSArrayとかNSDictionaryにつっこんどいたりできるってことでもある。コレクションクラスとか自前で作らなくても他のオブジェクトと一緒にArrayにつっこんどいてうまいこと処理するとかできる。ところでObjC上ではObjCのオブジェクト扱いだけど、Cで使ってるときはこれ何として扱われてるんだろう。気になる。

せっかくなのでブロック構文

これだけだと何それおいしいので終わりなので、実際に使ってるところをみてみる。Rubyでblockって言ったらブロック構文を思い浮かべると思う。例えばこんなの。

languages = ['Ruby','Objective-C','Perl','PHP','JavaScript','Haskell']
languages.each do |l|
  puts "#{l}なら多分書ける"
end

Blocksと言うからにはこういう使い方をするメソッドがいくつかFoundationのクラスにもある。上記のコードをObjCでブロックを使って書くとこんなかんじになる。

NSArray *languages = [NSArray arrayWithObjects:@"Ruby",@"Objective-C",@"Perl",@"PHP",@"JavaScript",@"Haskell",nil];
[languages enumerateObjectsUsingBlock:^(id item, NSUInteger idx, BOOL *stop) {
  NSLog(@"%@なら多分書ける", item);
}];

う、うん、似てる…よね?ちょっとわかり辛いかもだけど enumerateObjectsUsingBlock:ってのが引数にブロックを取るNSArrayのインスタンスメソッドで、Array#eachと同じように自分自身の要素を一個ずつ順にブロックに渡して実行してくれる。ちなみに、「最初の引数がNSArrayの要素のオブジェクト、次がインデックスの整数値、最後がループを止めるためのBOOLのポインタ」という3引数のブロックを渡さないといけない。どっちかっつーとEnumerable#each_with_indexのが近いかな。

上の例ではitemしか使ってないけど、全部使うとしたらこんな感じ。3個目の引数にYESを入れてやるとそれ以降は実行されなくなる。

[languages enumerateObjectsUsingBlock:
  ^(id item, NSUInteger index, BOOL *stop) {
    NSString *status;
    if ([item isEqualToString:@"PHP"]) {
      status = @"アタシはしんだ。";
      *stop  = YES;
    } else {
      status = @"楽しかった。";
    }
    NSLog(@"%d日目。%@を書いた。%@", index+1, item, status);
  }
];

や、別にPHPについて何か言いたいわけじゃないですよ?やだなぁ、ちょっとした冗談じゃないですか。

今度書く

この記事の例では書いてないけど、所謂クロージャなのでブロックが生成されたコンテキストの変数とかブロックの中から参照できる。NSArrayにmapメソッド生やしてみたり簡易ベンチマーク関数作ってみたりしたので今度晒す。

あと、Enumeratorもいいけど、Blocksが真の力を発揮するのは、Cならdispatch_async、ObjCならNSOperationとかを使って非同期に処理をぽんぽん投げてくとき。というか、BlocksがそもそもGrand Central Dispatchという仕組みと一緒に導入されたものなので、それについてはもうちょい調べて今度書く。ちなみに例えばNSArrayでもenumerateObjectsUsingBlock:の他にenumerateObjectsWithOptions:usingBlock:というメソッドがあって、これにNSEnumerationConcurrentってオプションを渡してやると並列に実行してくれたりする。

余談

構文がキモいのはもうObjCだしどうしようもないけど、気に入らなければtypedefしておいた上でブロック生成のマクロでも書いとくと少しはマシに見えなくもないような気がしなくもないような感じがしたりしなかったり。うん。キモい。

余談2

gccだと独自拡張らしいので多分使えるのMacOSX 10.6のXcodeに付属してるやつだけだと思うけど、Clangには組み込まれてるらしい。試してないけど、Clangでなら他のプラットフォームでもBlocks使ったCのコードコンパイルできるんじゃないかな。

余談3

iPhoneSDKで使えるのかい?ってのは聞かないでくだしあ。泣きたい。

お掃除

iPhoneアプリのコードのお掃除をしている。やれiPadだ新型iPhoneだOS4.0だ、うはぁ頼みの汎用ライブラリはUndocumentedなAPI使ってて動かない、なんですってー他の言語からのトランスレータは規約で禁止だってー、とまぁなんだかんだでiPhoneデベロッパはアプリの改修に追われててAppleに恨みつらみが募ってたりもするんじゃないかと思うけど、まぁ俺も大体そんな感じです。正確に言うとそうじゃなくてもしょっちゅう直してますけど。

リファクタリング

そんなわけで内部のコードの整理とかバグ取りとかついでに切り出したAtomPubクライアント汎用フレームワーク化しようかなーとかやってて、そうなるとちゃんとしたテスト書いてないと辛いのでOCUnitでテストケースをもりもり増やしてるんだけど、非同期にAPIと通信してるところがいまいちテストし辛い。というわけでNSURLConnectionを使ってるところを上手いことテストできないか試行錯誤してみた。

スタブに差し替えておく

まず実際にリクエストを投げないように、NSURLConnectionの実装を差し換える。実装の差し替えって言ったらposeAsClass:あたりかなーとか思ってたら、もうとっくにdeprecatedになってたのね。っていうかそういやそんな話聞いたの大分前だな。

そうするとRuntimeAPIのmethod_exchangeImplementationsあたりでメソッドを差し替えるのがいいのかなー。こんな感じか。

#import <objc/runtime.h>
#import <Foundation/Foundation.h>

void swizzle(Class target,SEL orig_sel,SEL alt_sel) {
    Method orig_method = class_getInstanceMethod(target,orig_sel);
    Method alt_method = class_getInstanceMethod(target,alt_sel);
    method_exchangeImplemantations(orig_method,alt_method);
}

これで例えば、

@interface NSURLConnection (ForTest)
- (void)_stab_start;
@end
@implementation NSURLConnection (ForTest)
- (void)_stab_start {
    // 元の実装だとこれを呼ぶと通信が始まるので、
    // 通信しないで直に
    // [self.delegate connectionDidFinishLoading:self]
    // とか
    // [self.delegate connection:self didFailWithError:error]
    // とかを送る処理を書く。
    // ただ非同期に処理が走(ってるように見せ)る必要があるので、
    // NSTimerとかを使って時間差で呼ぶようにしておく。
}
@end

みたいにしておいて、テストケースの方で

- (void)setUp {
    swizzle([NSURLConnection class], @selector(start), @selector(_stab_start));
}
- (void)tearDown {
    swizzle([NSURLConnection class], @selector(start), @selector(_stab_start));
}
- (void)testConnection {
    id hoge = [[Hoge allco] init]; // 内部でNSURLConnectionを使ってるクラス
    // 通信してますよー的な処理
    [[NARunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];
}

とかやればいいはず。load時じゃなくてsetUpで差し替えてtearDonwで戻してるのは、他のテストにまで影響が出ないように。これでいいんだっけ。差し替えられたら以後そのセレクタになるはずだから戻すときは同じのをもう一度呼べばいいんだよね、多分。

○○はループ説(ひぐらし的な意味で)

ちなみに、テストケース中にそのまま非同期通信を含む処理を書いても上手く動かない。NSTimerとかNSThreadとかを使う処理はNSRunLoopが回ってるときじゃないと期待する動きをしない(例えばNSTimerは発火しないのでタイマーから呼ばれるはずの処理はそもそも通らない)んだけど、テストケースの実行はアプリケーションじゃないのでもちろんNSRunLoopは回ってない。NSURLConnectionの実装を差し替えたおかげで処理にどのくらいの時間がかかるか予想できるので、適当な時間を指定してrunUntilDate:とかで走らせてやればちゃんと動く。

あとは普通にテストケース増やしてけばビルドする度に網羅的にテストが走るので大分安心感が増す。しかしUIの方はどうしたもんかね。こればっかりは見ながらデバッグするしかないのかなー。世のデベロッパのみなさんはどうしてるんだろう。

こないだAtomPubとWebDAVの話をしてて、あーそうかAPIをWebDAVとして実装してたらファイルシステムとしてマウントできて面白かったかもなぁ、でも実装面倒なんじゃないのかなどうだろやったことないし、でもvimで記事書いて:wで投稿されてmvしたらカテゴリ変わってrmで記事削除されてとか何それ胸膨らむ、じゃなかった夢膨らむし胸踊るね、なんて考えたところでふと気付いた。あるじゃん、ファイルシステムじゃないものをクライアント側でファイルシステムにしちゃう仕組み。

そうだ、FUSEだ。Filesystem in Userspace。前々からそのうち遊んでみようとは思ってたものの特にネタも思い付かなかったので手をつけてなかったのだけど、BlogFSに丁度良さそうだし、ちょっとやってみることにした。

Rubyでいじる

FUSEってファイルシステム作るくらいだからやっぱCとかC++とかで書くもんなんだろう、とぼんやり思ってたんだけど、そう言えば前にハチロクの卒研発表会でRubyで実装したとか言ってたよなぁ、あとhatenafsはそう言えばPerlだったっけなぁ、と思い出した。なんだ普段使ってるので書けるな。軽くググったらRubyでもFUSEのライブラリがあるらしい。よし。

…makeでコケた。んー。Rubyのfusefsに問題があるのかもしれないしMacFUSEだからそのせいかも知れないしなんかfusefsの情報古いのしかでてこないなーあー面倒くさいや。どうしたもんかね。

僕らのMacRubyさんがいるじゃないか

RubyからFUSEを使う方法をもう少し調べるとか、Perlから使う方法を調べるとか、そっちに進んでも良かったんだけど、もっとお手軽な方法に気が付いた。MacFUSEをインストールするとMacFUSE.frameworkが入る。てことは、とりあえずObjCからは簡単に使える。ObjCで書くのはまぁいいけど、正直AtomPubの扱いとか文字列の処理とか面倒くさすぎる。あれ、ちょっと待て。ObjCのフレームワークがあるってことは…MacRubyから使えるじゃないか。

$ ls /Library/Frameworks
. .. MacFUSE.framework MacRuby.framework UIM.framework
$ macruby -v
MacRuby version 0.5 (ruby 1.9.0) [universal-darwin10.0, x86_64]
$ macirb
irb(main):001:1 > framework 'MacFUSE'
=> true

素敵!愛してる!

とりあえずHelloFS

MacFUSEのサンプルとか、「雑草ブログ Luaでファイルシステムを実装しよう(MacFUSEで)」あたりを参考にとりあえず実装してみた。

これをmacrubyコマンドで実行する。ちなみにSnow Leopard+MacRuby 0.5beta1+MacFUSE 2.0.3でやってるけど、Leopard+MacRuby 0.4でも動くとは思う(未確認)。

$ macruby namakefs.rb &
$ ls /Volumes/NamakeFS
honne.txt
$ cat /Volumes/NamakeFS/honne.txt
寒いので冬眠したい

OK、OK。上出来。ちなみにこれだけでFinder上にもNamakeFSディスクがマウントされて、ちゃんとCocoaアプリとかからでも開ける。思ったより簡単だった。HotCocoaでアプリ化しちゃえば配布できる形にするのも楽だし、ファイルシステムを作るの自体はObjCの作法に則る必要があるけど、中のロジックはRubyで書けるから、RubyのAtomPubのライブラリも使えるしObjCだと本当に面倒な文字列の処理もさくさく実装できる。

MacRubyでやるってのがまぁ難点と言えば難点だけど。正直開発環境としてはまだアレげだし。RubyCocoaでやりゃいいって話ではあるんだけど、ねぇ…まぁ、いっか。

あんまり関係ないけど、Luaが意外とすんなり読めた件。まぁ中でやってることはまんまObjCだからってのもあるけど。ちょっとLuaもいじってみようかしら。

なんでもターミナルで完結させたい病

最近Objective-Cをお仕事で書いてたりするんだけど、試行錯誤しながらじゃなくてある程度量書くようになってくるとXcodeじゃかったる過ぎるので、ObjC対応済みのctagsをインストールしたりxcodebuildをよしなに叩いてくれるRakefile書いたりしてvimで作業する環境を整えた。もちろんiPhoneシミュレータを起動したりリファレンス読んだりするのでXcodeは開きっぱなしなんだけど、それでも書くのをvimでやれるようになったら大分楽になった。

書くのは楽になったけど、そうなると今度は動作の検証もいちいちシミュレータ起動してあれこれ操作してってのが馬鹿馬鹿しくなってくる。そもそもテストも書かないでアプリ開発とか泣けてくる。幸いiPhoneSDKでもOCUnitが使えるので、ライブラリとして切り出せる部分を別プロジェクトに切り出した上でユニットテストを書くことにする。OCUnitがUnitTest Bundleっていうテンプレートを用意してくれてるので、プロジェクトにUnitTestのターゲットを追加してそれをビルドするだけでテストが走るようになっている。これならrakeからも簡単に実行できていい。

読めないよ

が。OCUnitでのテストをやったことある人は知ってると思うけど、OCUnitの実行結果はそんなに見易くない。

ocunitbuild

読めるかっつー話。もちろんこれはXcode上でも表示されるので、何の装飾もないのも当然。UnitTest Bundleのターゲットをビルドしたときに走るカスタムスクリプトを自分でごにょごにょすればいいっていえばいいんだけど、同じXcodeのプロジェクトを他の人がいじったりすることも有り得るのであんまりそっちで特殊なことをしたくない。Xcode上でビルドしたときはこれはこれでいいのだし。かといってそのままならビルドしてテストまで走らせてくれるようになってるのに、自前でビルドしてパッケージしてテストして表示するRakeタスクを作るのもなんだかなあ。それ両方メンテするの途中で面倒になって結局どっちか放置しそう。

見辛いなら見易くしてやればいいのです

というわけで、xcodebuildを実行した結果を綺麗に整形して表示するスクリプト書いた。さっきのテストがこんな感じに表示される。

rocu

こういうのでいいんだよ、こういうので。緑色になってるとテンション上がるだろ。

ちなみに、STAssert系、NSAssert系のアサーションや、実行時のエラーなどもちゃんと拾う(拾ってくれなきゃ意味ない)。failさせるとこんな感じになる。

rocu_fail

STAssertでこける、つまりテストケースで期待した結果が出てないときは赤字でアサーションのメッセージを出す。アプリ内のアサーションでひっかかってるときは黄色。それ以前にランタイムエラーで実行中に落ちる場合は赤の太字。ちなみに別に自前でテストツール作ったわけじゃないので(あくまでxcodebuild実行時の出力をフィルタしてるだけ)、ランタイムエラーがどこで出てるかまではわからない。そういうのはXcodeのデバッガとかでやってくだしあ。でもNSAssert系のマクロを使うとこけた場所も取れるので、ある程度厳密にやっておきたい値の検証とか積極的にNSAssert使うといいと思う。OCUnitの実行時にはNSLogでprint debugってわけにも行かないしねぇ。

一応コマンドにしてgistに上げてみた。色付き表示にするにはTerm::ANSIColorがいるので注意。色がいらなければ無くてもいい。使い方はソース読むといいと思います。元々Rakefileの中に書いてたのを抜き出してきてやっつけでコマンドにしたやつなので、全然動作の検証とかしてない。なんかツール郡にまとめようかと思ったけど満足しちゃったので。

MacRubyとRubyCocoaの微妙な違い (1)MacRubyとRubyCocoaの微妙な違い (2)の続き。

クラスをいじってみる

またまた抜粋

  # MacRuby版
  it 'Objective-Cのクラスを書き換えられること' do
    lambda { eval 'class Sloth; def name; "nisefaultier"; end; end' }.should.not.raise
    obj = Sloth.alloc.init
    obj.name.should.be.eql "nisefaultier"
    # @obj.name.should.be.eql "nisefaultier"
  end
  # RubyCocoa版
  it 'Objective-Cのクラスを書き換えられること' do
    lambda { eval 'class Sloth; def name; "nisefaultier"; end; end' }.should.not.raise
    obj = Sloth.alloc.init
    obj.name.should.be.eql "nisefaultier"
    @obj.name.should.be.eql "nisefaultier"
  end

違いは一点、MacRuby版でクラスの定義を書き換えたとき、新しく作られるオブジェクトには影響するけど、既にあるオブジェクトには影響しない。が、RubyCocoa版はクラスの定義を書き換えるとそのクラスの全てのオブジェクトに影響する。面白い。Rubyは全てのオブジェクトがメタクラスのオブジェクトを見てるけど、ObjCのオブジェクトはそれぞれが関数ポインタか何かでメソッドを保持してるってことかな?RubyCocoaにおける「Cocoaのオブジェクト」って結局「(ObjCのオブジェクトを内部に持った)Rubyのオブジェクト」だけど、MacRubyのオブジェクトは全てが「ObjCのオブジェクト」なので、若干CRubyと挙動が違ってくるみたい。

ところで、上のコードはインスタンスメソッドを書き換えてるんだけど、ふと思い立ってこんなコードを書いてみた。

$ cat mrb_class.rb 
framework 'build/Release/Namake.framework'

p Sloth.new

class Sloth
  def self.new
    "string"
  end
end

p Sloth.new
$ macruby mrb_class.rb 
#<Sloth:0x80063c740>
"string"

MacRubyだと、Sloth::newの実装が差し換えられた。RubyCocoaだとどうだろう。

$ cat rbc_class.rb
require 'osx/cocoa'
include OSX

bundle = NSBundle.bundleWithPath('build/Release/Namake.framework')
bundle.load

p Sloth.new

class Sloth
  def self.new
    "string"
  end
end

p Sloth.new
$ ruby rbc_class.rb 
#<OSX::Sloth:0x2385ac class='Sloth' id=0x3734a0>
"string"

あれっ。別に普通だ。同じだった。というのも、昔RubyCocoa+RSpecでごにょごよやってたときに、Cocoaのクラスにstubを仕込んだら「それはObjC側の実装だから勝手に変えるな」みたいに怒られた記憶があるんだけど、んー、この感じだと特に問題無さそう?っていうかあれか、RSpecのstub!はもっと中で複雑なことしてるんだよなきっと。こんな単純な話じゃなくて。後で読んでみよう。もしかしたらそこが「RubyCocoaには無理だけどMacRubyならできる」とこかもしれないし、逆にMacRubyだと下手にいじると大変なことになる罠だったりするかもしれないし。

試してないこと

意図的に上の例では抜いてるけど、MacRubyの大きな特徴と言えば、「キーワード付き引き数」。そもそもそんな機能自体Rubyには無いので、これを上手く使えたら大分面白いことになりそう。あと「CocoaのクラスのサブクラスをRuby側で定義する」とか。MacRubyは何度も言うけど「全部がNSObjectのサブクラス」なので、ぶっちゃけ普通のRubyスクリプトを書くだけでもそういう風になってるんだけど、RubyCocoaはそうではないので、なんか違いがあるんじゃないかと思ってみたり。

そう言えば

MacRubyではオブジェクトを「release」することができない。undefined methodで例外が出る。まぁそもそもGC有りなのでオブジェクトにreleaseを送っても無意味なんだけど。だから存在する必要がないし「undefined」で当然か。ちなみに、RubyCocoaだとCocoaのオブジェクトにはちゃんとreleaseメッセージを投げられる。もっともRubyのGCでオブジェクトが開放されるときに自動的に呼ばれてるはずなので、RubyCocoaのコードでreleaseする必要もやっぱりない、はず。むしろ予想外のバグになりそうなのでやらない方がいいのかも[要出展]。

MacRubyとRubyCocoaの微妙な違い (1)の続き。

フレームワークの読み込み

まず最初に違うのは、framework。これはRubyCocoaで書くと多分こんな感じ。

require 'osx/cocoa'

def framework(name)
    OSX.require_framework name
rescue
    bundle = OSX::NSBundle.bundleWithPath(name)
    bundle.load
end

MacRubyのframeworkは名前じゃなくてパスを渡してやるとそのFrameworkを読み込んでくれるけど、OSX::require_frameworkは名前しか受け付けない、つまりパス上にあるFrameworkしか探さない。ので、任意のパスにあるFrameworkを動的に読み込みたいときは、NSBundleのクラスメソッドを使ってバンドルのオブジェクトを作っておいて、それのloadメソッドを呼ぶ必要がある。ちなみにこれはRubyCocoaではなくCocoaの方の機能なのでもちろんMacRubyでも使える。

Stringの扱い

RubyCocoa版の方で、Slothクラスのname、setNameを呼び出してるとこだけ抜粋。

  it 'Objective-Cのメソッドを呼べること' do
    @obj.name.should.be.nil
    # lambda { @obj.name = "faultier" }.should.not.raise
    lambda { @obj.name = "faultier".to_ns }.should.not.raise
    @obj.name.should.be.== "faultier"
    # @obj.name.should.be.eql "faultier"
    # @obj.name.should.be.kind_of String
    @obj.name.should.be.kind_of NSString
    lambda { @obj.setName "ふぁうるてぃあ".to_ns }.should.not.raise                     
    @obj.name.to_s.should.be.== "ふぁうるてぃあ"
  end

コメントアウトしてるところは、実行するとテストが通らないところ。RubyCocoaではStringとNSStringは全く別のクラスなので、引き数にNSStringが来るはずのCocoaのメソッドを呼ぶときはRubyのString#to_nsを呼んで一度NSStringのオブジェクトに変換してから渡してやる必要がある。また、当然kind_of?(String)はfalseになる。面白いのは、

"string".to_ns == "string" #=> true

になるってこと。==のときは変換かけてから比較するらしい。もう一つ面白いのは、asciiのときにはStringとNSCFStringのeql?がtrueになるのに、日本語のときは何故かfalseになる。$KCODEを指定してやっても駄目。ふぅん。あれ。ていうか、"string".to_ns.eql?("string") ってそもそもtrueじゃないよな。なんでshould.be.eql("string")だと通るんだろう。通っちゃう方がおかしい気がする。

全然そんなことなかった。@obj.name.to_s.should.be.eql("faultier")にしてた。そりゃ通るわ!というわけで訂正。==はNSStringとStringの変換を行なった後比較するけど、eql?およびequal?はもう少し厳密で、NSStringとStringを別のものとして扱う。マルチバイトでも特に違いはない。

ちなみに、MacRubyでは以下が全て成立する。

  it 'Objective-Cのメソッドを呼べること' do
    @obj.name.should.be.nil
    lambda { @obj.name = "faultier" }.should.not.raise
    @obj.name.should.be.== "faultier"
    @obj.name.should.be.eql "faultier"
    @obj.name.should.be.kind_of String
    @obj.name.should.be.kind_of NSString
    lambda { @obj.setName "ふぁうるてぃあ" }.should.not.raise
    @obj.name.should.be.== "ふぁうるてぃあ"
    @obj.name.should.be.eql "ふぁうるてぃあ"
  end

理由は簡単。

$ ruby -r'osx/cocoa' -e 'p String.ancestors'
[String, Enumerable, Comparable, Object, Kernel]
$ ruby -r'osx/cocoa' -e 'p OSX::NSString.ancestors'
[OSX::NSString, OSX::NSObject, OSX::OCObjWrapper, OSX::NSKeyValueCodingAttachment, OSX::NSKVCAccessorUtil, OSX::ObjcID, Object, Kernel]
$ macruby -e 'p String.ancestors'
[NSMutableString, NSString, Comparable, NSObject, Kernel]
$ macruby -e 'p NSString.ancestors'
[NSString, Comparable, NSObject, Kernel]

見ての通り、MacRubyではRubyのStringはNSStringのサブクラスになってる。てことは、Slothクラスのname/setNameにはRubyのStringオブジェクとがそのまま渡ってそのまま返ってきてるはず。この辺は流石にMacRubyの方がずっと自然に感じられる。

さらに続く。なんか妙に筆が進んでしまった。

MacRubyでBaconを調理する準備ができたので、早速Bacon焼くぜー超焼くぜー、と書き始めたんだけど、その前にRubyCocoaとMacRubyの違いを比べてみたくなった。のでちょっと脱線。

# Namake.framework内にあるSloth.h
#import 

@interface Sloth : NSObject {
    NSString *name;
}

- (NSString *)name;
- (void)setName:(NSString *)aName;

@end
$ cat mrb_bacon.rb
# coding: utf-8
require 'bacon'

framework 'build/Release/Namake.framework'

describe Sloth do
  before do
    @obj = Sloth.alloc.init
  end

  it 'Objective-Cのオブジェクトであること' do
    @obj.should.be.kind_of Sloth
    @obj.should.be.kind_of NSObject
  end

  it 'Objective-Cのメソッドを呼べること' do
    @obj.name.should.be.nil
    lambda { @obj.name = "faultier" }.should.not.raise
    @obj.name.should.be.== "faultier"
    @obj.name.should.be.eql "faultier"
    @obj.name.should.be.kind_of String
    @obj.name.should.be.kind_of NSString
    lambda { @obj.setName "ふぁうるてぃあ" }.should.not.raise
    @obj.name.should.be.== "ふぁうるてぃあ"
    @obj.name.should.be.eql "ふぁうるてぃあ"
  end

  it 'Objective-Cのクラスを書き換えられること' do
    lambda { eval 'class Sloth; def name; "nisefaultier"; end; end' }.should.not.raise
    obj = Sloth.alloc.init
    obj.name.should.be.eql "nisefaultier"
    # @obj.name.should.be.eql "nisefaultier"
  end
end
$ macbacon mrb_bacon.rb
Sloth
- Objective-Cのオブジェクトであること
- Objective-Cのメソッドを呼べること
- Objective-Cのクラスを書き換えられること

3 specifications (11 requirements), 0 failures, 0 errors
$ cat rbc_bacon.rb
require 'bacon'
require 'osx/cocoa'
include OSX

bundle = NSBundle.bundleWithPath('build/Release/Namake.framework')
bundle.load

describe Sloth do
  before do
    @obj = Sloth.alloc.init
  end

  it 'Objective-Cのオブジェクトであること' do
    @obj.should.be.kind_of Sloth
    @obj.should.be.kind_of NSObject
  end

  it 'Objective-Cのメソッドを呼べること' do
    @obj.name.should.be.nil
    # lambda { @obj.name = "faultier" }.should.not.raise
    lambda { @obj.name = "faultier".to_ns }.should.not.raise
    @obj.name.to_s.should.be.eql "faultier"
    # @obj.name.should.be.kind_of String
    @obj.name.should.be.kind_of NSString
    lambda { @obj.setName "ふぁうるてぃあ".to_ns }.should.not.raise                     
    # @obj.name.should.be.eql "ふぁうるてぃあ"                                          
    # @obj.name.should.be.eql "ふぁうるてぃあ".to_ns                                    
    # $KCODE = 'u'
    # @obj.name.should.be.eql "ふぁうるてぃあ"
  end

  it 'Objective-Cのクラスを書き換えられること' do
    lambda { eval 'class Sloth; def name; "nisefaultier"; end; end' }.should.not.raise
    obj = Sloth.alloc.init
    obj.name.should.be.eql "nisefaultier"
    @obj.name.should.be.eql "nisefaultier"
  end
end
$ bacon rbc_bacon.rb
OSX::Sloth
- Objective-Cのオブジェクトであること
- Objective-Cのメソッドを呼べること
- Objective-Cのクラスを書き換えられること

3 specifications (12 requirements), 0 failures, 0 errors

上がMacRuby版、下がRubyCocoa版。ぱっと見は思ったより違いはない。まぁgetter/setterしかないクラスなのでアレだけども。RubyCocoaの方はCocoaのクラスはOSXって名前空間の下にあるけど、MacRubyは全てのRubyのクラスがCocoaのクラスのサブクラスって位置付けなので、Cocoaのクラスに特別な名前空間は無い。もともとCocoaのクラスはNSなんとかとかUIなんとかとかそういうprefixが付いてるので、Rubyの組み込みのクラスとバッティングすることはないのでそれでも困らないしね。まぁ、それはRubyCocoaの方でもinclude OSXしてやれば見ためそんなに変わらないので、そこは然程違和感はないね。

長いので分割して書くことにしよう。次に続く。

以前、「Objective-CのテストやるのにOCUnit使うよりも、RSpec使えたら素敵なんじゃね」と思って、色々試行錯誤してみたことがある。

とりあえずそれっぽいことができるところまでは行ったものの、そのあと特に使う機会もなくそのまま放置していたのだけど、今度は本格的にObjective-Cのフレームワークを作らなきゃならなくなりそうなのでまたRubyCocoaなどいじる。そこでふと思い付いた。せっかくMacRubyがあるんだから、今度はMacRubyで同じことをやってみたらどうか、と。RubyとObjCのブリッジであるRubyCocoaよりも、VMのレベルでObjCと融合してるMacRubyのが色々融通効くんじゃね、もしかしたら結構深くObjCのオブジェクトいじりたくなるかもしんないし。

あれ、読み込めない

というわけでまずはObjCのFrameworkを作ってRubyのスクリプトから読み込ませてみる。NSBundle#loadを呼びだすのでもいいけどMacRubyだったらframeworkって構文があるな、これフルパス指定してあげれば任意のFramework読み込めるみたい。

こういうクラスを含むNamake.frameworkというのを作ったとして、

#import 

@interface Sloth : NSObject {
    NSString *name;
}

- (NSString *)name;
- (void)setName:(NSString *)aName;

@end

それを使うRubyスクリプトはこんなふうに書く。

#!/usr/local/bin/macruby

framework 'build/Release/Namake.framework'
# こっちでもOK
#bundle = NSBundle.bundleWithPath('build/Release/Namake.framework')
#bundle.load
# /System/Library/Frameworks以下にあるフレームワークとかだったら、
# フレームワーク名だけで行ける
#framework 'Cocoa'

sloth = Sloth.new # Sloth.alloc.init
sloth.name = 'faultier'
p sloth
puts sloth.name

framework 'hoge'のお陰ですっきりして見える。紛うことなきRubyのコードだ。早速実行。

$ macruby mrb_test.rb
2009-04-18 22:19:09.756 macruby[773:10b] Error loading /Users/faultier/Projects/Namake/build/Debug/Namake.framework/Namake:  dlopen(/Users/faultier/Projects/Namake/build/Debug/Namake.framework/Namake, 265): no suitable image found.  Did find:
	/Users/faultier/Projects/Namake/build/Debug/Namake.framework/Namake: mach-o, but wrong architecture
./mrb_test.rb:3:in `framework': framework at path `build/Debug/Namake.framework' cannot be loaded: Error Domain=NSCocoaErrorDomain Code=3585 UserInfo=0x800621ee0 "The bundle “Namake” could not be loaded because it does not contain a version for the current architecture." (dlopen_preflight(/Users/faultier/Projects/Namake/build/Debug/Namake.framework/Namake): no suitable image found.  Did find: (RuntimeError)
	/Users/faultier/Projects/Namake/build/Debug/Namake.framework/Namake: mach-o, but wrong architecture)
	from ./mrb_test.rb:3:in `<main>'

おおぅ…。何、何言ってるの。architectureがおかしいってなんだよ。だってこれRubyCocoaや素のObjective-Cからはちゃんと読めるよ…?

で、調べてみたところ、MacRubyはデフォルトでは64-bitのバイナリを読むらしい。なるほど。なので、↑のを実行するときに、archで-i386を指定してやると一応実行できる。

$ arch -i386 macruby mrb_test.rb
#<Sloth:0x12d06d0>
faultier

もうちょいちゃんとした解決策としては、ターゲットにx86_64が含まれるようにビルドすればいい。あと、MacRubyはObjCのGCを使ってるので、GCサポートを有効にしてビルドしてないと勿論読み込めないので注意。

macgemがアレげ

さてフレームワークはちゃんと読み込めた。そしたら今度はRSpecだ。えーと、macruby系のコマンドはmacってprefixが付くのでgemはmacgemか。

$ sudo macgem install rspec
Password:
ERROR:  Error installing rspec:
	rspec requires  (, runtime)

えー…。ここでも詰まるか。一応調べては見たけど、なんかバグレポートにも上がってたし、これはまだ未解決なのかな。そうでなくてもRSpecは案外依存関係が多くて、macrubyみたいな環境で使おうと思うとあっちのパスを直してこっちのライブラリを直して、みたいなことになりそうなのでちょっと心が折れかける。

で、しかたないなーtest/unit使うのかーとかぼやいてたら「そんなあなたにBacon」と言われたので、Baconを入れてみることにした。RSpecっぽい構文だけど最低限の要素に絞ってシンプルにしたものらしい。Ramazeで使われているようだ。mockとかが入ってないのは大分物足りないけども、同じ機能でも見た目の差は考え方の差になる程度には大きい。背に腹はかえられない。

$ sudo macgem install bacon
Password:
Successfully installed bacon-1.1.0
1 gem installed
Installing ri documentation for bacon-1.1.0...
Updating class cache with 17 classes...
Installing RDoc documentation for bacon-1.1.0...

おお、サクっと入った。素敵。ところでbaconコマンドが見あたらないんだけどどこに行ったんだ、と思ってたら、

/Library/Frameworks/MacRuby.framework/Versions/0.4/usr/lib/ruby/Gems/1.9.1/gems/bacon-1.1.0/bin/bacon

こんなところにいらっしゃいました。確かにデフォルトのRubyから入れたコマンドを上書きされても困るし、これはこれでいい。他のコマンドにならってmacbaconって名前でPATHの通ってるとこにシンボリックリンクを張ってやることにする

$ macbacon --version
/opt/local/lib/ruby/vendor_ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require': no such file to load -- bacon (LoadError)
	from /opt/local/lib/ruby/vendor_ruby/1.8/rubygems/custom_require.rb:31:in `require'
	from /usr/local/bin/macbacon:88
	from /opt/local/lib/ruby/1.8/optparse.rb:1262:in `call'
	from /opt/local/lib/ruby/1.8/optparse.rb:1262:in `parse_in_order'
	from /opt/local/lib/ruby/1.8/optparse.rb:1249:in `catch'
	from /opt/local/lib/ruby/1.8/optparse.rb:1249:in `parse_in_order'
	from /opt/local/lib/ruby/1.8/optparse.rb:1243:in `order!'
	from /opt/local/lib/ruby/1.8/optparse.rb:1334:in `permute!'
	from /opt/local/lib/ruby/1.8/optparse.rb:1355:in `parse!'
	from /usr/local/bin/macbacon:93
	from /opt/local/lib/ruby/1.8/optparse.rb:787:in `initialize'
	from /usr/local/bin/macbacon:11:in `new'
	from /usr/local/bin/macbacon:11

ありゃ。ああそっか、多分shebangとかLOAD_PATHとか書き換えなきゃなんだな。

と、思ったんだけど、実はそんなことしなくても良かったことが判明。この次に書いてあることは実行せず、追記のところまで読んで下さい。

#!/usr/bin/env ruby
# -*- ruby -*-

require 'optparse'
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '../lib/')
module Bacon; end 

ビンゴ。ってことでこれを、

#!/Library/Frameworks/MacRuby.framework/Versions/0.4/usr/bin/macruby                    
# vim: filetype=ruby fileencoding=utf-8                                                 

require 'optparse'
$LOAD_PATH.unshift '/Library/Frameworks/MacRuby.framework/Versions/0.4/usr/lib/ruby/Gems/1.9.1/gems/bacon-1.1.0/lib/'
module Bacon; end

こんな風に書き換える。マジックコメントをvimにしたのは単に俺がvim使いだから。MacRubyは1.9.1相当なので、encodingはちゃんと指定しておいた方がいいな。パスがベタ書きなのは気持ち悪いと言えば悪いけど、まぁ移動することも無いので別にいいか。

$ macbacon --version
bacon 1.1

できたできた。ふー。このくらいのとこまではmacgemでやってくれるようになったら嬉しいんだけど、なんつーか面倒臭そうではあるね。とにかくこれでMacRuby+Bacon環境が出来たので少し休憩。

追記

上の作業をする前にデフォルトのRubyの方でもBaconを入れてたんだけど、今度はそっちの挙動が何か変だ、と思ったら…

$ cat /usr/bin/bacon
#!/Library/Frameworks/MacRuby.framework/Versions/0.4/usr/bin/macruby
#
# This file was generated by RubyGems.
#
# The application 'bacon' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require 'rubygems'

version = ">= 0"

if ARGV.first =~ /^_(.*)_$/ and Gem::Version.correct? $1 then
  version = $1
  ARGV.shift
end

gem 'bacon', version
load 'bacon'

おい。待てコラ。さっき「デフォルトのRubyから入れたコマンドを上書きされても困るし」って書いたけど、しっかり上書きされてました。あうー。まぁでもこれを使えば上に書いたような気持ち悪い書き換えはしないで済むみたいだ。ので、

  1. さっき書き換えたところは全部元に戻し
  2. /usr/bin/baconを/usr/local/bin/macbaconにrenameし
  3. 再度/usr/bin/gemの方でBaconをインストール

した。と言うわけで、デフォルトのRuby(/usr/bin/ruby)を使ってる人はmacgemの実行は気を付けた方がいいみたい。まぁ、macgem自体は今殆どのgemがインストール時にコケるし、デフォルトのRubyはバージョンが若干古いので基本的には自分でbuildして/usr/local/binあたりかMacPortsから/opt/local/binあたりに入れてると思うので、そんなに困ることはないかもしれないけど、念のため。

おまけ

Baconってなんだろう、ってググったら当然ベーコンに関する情報がヒットするんだけど、Google先生がなんか妙なものを拾ってきた。

bacon

右端の変態、あんた何やってんだ。

俺もメモリ管理を間違えて頻繁にアプリを落としてしまうゆとりプログラマなのであんまり偉そうなことも言えないのだけど、「releaseの使いどころ。メモリの辺りがどうしてもわからない。」を読んでいくつか思ったところがあるので書いときます。

「何度もinit」はしない

既に確保されているハズの変数を二度initするのはメモリリークだよね?
解放されている変数は、
if (obj == nil) では判別できないのだろうか。
ボタン押下時などに、同じロジックを走ることが多くて、何度もinitをしてしまうことが多いのだが。。。。

具体的なコードを見てないので間違ってるかもだけど、多分これ「何度もinitしちゃう」じゃなくて「開放してないオブジェクトを参照してる変数に、何度も新しいオブジェクトをつっこんじゃう」ってことだと思って話を進める(そうすると、どこからも参照が辿れないのにメモリが開放されてないオブジェクトができちゃうからメモリリーク)。ちなみにもし本当にinitメッセージを何度も送ってるんだとしたら、

  • allocとinitを必ずワンセットで使う
  • initはオブジェクトが生成された後絶対に一度しか呼ばない

ってのを守るだけでいい。[[Class alloc] init]はイディオムなのでinitだけ分けて後から呼ぶことはそうそうないし、すべきじゃない。

前者の、例えば「そのメソッドを通るときにインスタンス変数に新しいオブジェクトをつっこむ処理がある」とかだったら。仮にこういうクラスだったとして、

#import 

@interface Hoge : NSObject {
   NSArray *array;
}
- (void)foo;
- (void)bar;
@end

(こんなのは流石にやらんけど、例えば)その実装がこういう風だとメモリリークが起きる。

- (void)foo
{
    // fooの前に必ずbarが呼ばれてればいいけど、
    // そうでないなら元々arrayに入ってたオブジェクトがリーク。
    array = [[NSArray alloc] init];

    /*
     * [array release]を含まないなんかの処理
     */
}
- (void)bar
{
    // arrayのretainCountが1だったらこの時点でarrayはnilになるけど、
    // どっか他でretainされてたらnilにはならない
    [array release];
}

基本的にはそこを通る前に必ずreleaseしてあるようにしてなきゃ駄目だけど、「念のためそのインスタンス変数に代入する前に必ずreleaseを送る」とかするのもいいと思う。そうすれば代入する時点では必ず開放されてるのは間違いないし、仮に既に開放済みで変数の中身がnilだったとしても、nilにどんなメッセージを送っても「何も起こらない」ので特に問題ない。

- (void)foo
{
    [array release]; // もしarrayがnilならこの行は何もしてないのと同じ
    array = [[NSArray alloc] init];

    /*
     * [array release]を含まないなんかの処理
     */
}

もちろんif (obj == nil)で判別できるので「objがnilの時だけ代入する」でも問題無いはずだけど。どっか他でretainしてるとかでその変数がnilになってない可能性があるなら、release時点で明示的にnilを入れておけばいい。その場合はそのオブジェクトは消えないままその変数がnilになるので、当然そのオブジェクトをretainしてる別なオブジェクトが責任持ってreleaseしなきゃ駄目だけどね。

- (void)foo
{
    if (array == nil)
    {
         array = [[NSArray alloc] init];
    }
    /*
     * [array release]を含まないなんかの処理
     */
}
- (void)bar
{
    [array release];
    array = nil; // barが実行されたら、必ずarrayはnilになる
}

initで確保してないNSStringは解放しない

これは「しなくていい」じゃなくて「しちゃいけない」。stringWithなんたらとかいう類のstringを生成して返すメソッドはNSStringのオブジェクトを先にautoreleaseしてから渡してくれるんだけど、autoreleaseされてるオブジェクトを明示的にreleaseするとエラー出てアプリごと落ちるので気を付けた方がいい。NSArrayのarrayとかNSDictionaryのdictionaryとか、クラスメソッドでオブジェクトの初期化までやってくれるやつも基本的にどれも「allocしてinitしてautorelease」相当の処理をやってるので同様。多分「変なとこでreleaseしたせいで落ちた」ってのの原因のかなりのとこがこれじゃないかと思うので、autoreleaseされてるオブジェクトにrelease送ってないか見てみるといいと思う。というか、俺がそれを頻繁にやらかしてる人のなので…。ついやっちゃう。

int main(int argc, char **argv)
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    NSString *hoge = [NSString string]; // ちなみにこれはautoreleaseされてる
    NSString *fuga = [[NSString alloc] init];
    NSString *piyo = [[[NSString alloc] init] autolelease];

    // [hoge release];
    [fuga release]; // これは問題ない、というかやんないとダメ
    // [piyo release];

    // poolを開放するときに
    // autoreleaseされてるhogeとpiyoを開放しようとするけど、
    // 明示的にreleaseを呼んでしまっているとエラーが出る。
    // 試しに↑のコメントアウトを外すと落ちる。
    [pool release];

    return 0;
}

こんな感じ。あと、

また更に外部のobjectに渡すと、構わずreleaseするとアプリが落ちたりするので解放できなくなったりして、
解放するタイミングを失うのだが、そんなもん?!

ってあるけど、これもCocoaのクラスを参考に、自分で作るときもautorelease済みのオブジェクトを渡すようにしてやるといいと思う。autoreleaseしてしまえば生成した側も使う側もメモリの開放に責任持たなくて良くなるので、変なとこで開放しちゃったとか開放し忘れたとか起きにくい。もちろん勝手に消えるけど。

そうじゃなくて永続的に持ちたい場合は、使う側のオブジェクトがcopyするかretainするかして、そのオブジェクトの開放に責任を持つようにすること。「こっちで作ったオブジェクトをあっちで使ってるから安易に開放できなくて、でもこっちがあっちで」みたいなことが頻繁に起きて煩雑になるようなら、あるいはそれも設計を見直した方がいいかも。あんまり別なクラスのメソッドで生成されたオブジェクトを参照で受けてそれを共有して…ってのは良い設計じゃなさそうな臭いがする。

まとめ

  • 大前提として、これをできるだけ守る
    • 処理の中で生成したオブジェクトはなるべく外に出さずに処理の中でreleaseする
    • initしたやつはそれをreleaseする責任を持つ
    • retainしたやつはそれをreleaseする責任を持つ
  • allocとinitは必ずワンセット
  • autoreleaseされたオブジェクトにreleaseを送ってはいけない
  • オブジェクトの参照を受けとってそれを保持しておくような処理をするときは、受けとった側がcopyするかretainする
    • もちろん必ず受けとった側でも要らなくなったときにreleaseする
  • 「処理をする前に一度release」と「releaseしたあとnil代入」は結構使える

要は「initやretainはretainCountを1増やす、releaseはretainCountを1減らす」なのでその対応関係をしっかり保とうってのと、autorelease済みのオブジェクトにrelease送ってないか気を付けとこう、って話でした。

とか記事書いてたら

コメント付いてた。「たのしいCocoaプログラミング」は良い本です。さしあたりそれと「詳解Objective-C2.0」は持ってて損しないです。

余談

考えると当たり前なんだけど、autorelease多用してるとその分パフォーマンスは落ちるしメモリも食うと聞いた。のでiPhoneアプリではなるべくautorelease使わないようにしてるけど、どうなんだろう。正直大して違わなかったりするのかな。あとこれは単に俺があたまよわいだけかもしれないけど、予期しないタイミングでオブジェクトが消えてたり、上に書いた「うっかりrelease送ってしまって全然違うところでエラーが出る」とかも起こりやすい気がするんだよなー。どっちかというとinit/retainとreleaseの方が好き。

最近Objective-Cばかり書いてるナマケモノです、どうもこんばんは。Cocoaに始めて触れたときはこれで仕事になるとか思ってもなかったけど世の中わからないものだなー。

ダメな子なので可愛かったりするのだけども

iPhoneアプリの開発もそろそろ佳境に入ってきてるのだけど、つくづく悩まされてるのがWebViewの不安定さ。実はなかなか使い勝手は良くて、Safariもどきとかならサクっと作れちゃうし、Webサービスと連携するアプリにしても困ったらサイトに行ってログインしてもらえばいいや、とか思ってたのだけど、いかんせん落ちまくるのがいただけない。Safariが駄目なんじゃないんだ。WebViewがそもそも駄目っぽい。

なんてことを痛感したおかげで、正直今までiPhoneやWindows mobile端末向けのWebUIってあんまり興味なかったんだけど(正直PC向けのページ見りゃいいじゃん、と思ってた)、ちょっとアレら向けに徹底的に最適化したUIを作らなきゃだめだなと思ってるところ。あと、API。WebViewで楽できないとわかった以上、ついつい後回しにしがちなAPIを口実付けて整理しなおして、ネイティブアプリはネイティブアプリで完結できるようにしないとだなぁ。とりあえずアプリのリリースができて一段落したら、モバイルの高機能端末向けのUIとAPIについてちゃんと考えたい。ブログとかクリップとか、ね。

あと全然関係ないけど

ARToolKitの本買った。あんま読めてないけど。最近浮気しすぎだなー。まぁ、今のうちにインプットできるだけインプットしとかないと。

今とある事情でObjective-Cの基礎についての説明を書いている。むーん。言語の特徴や基本的な要素を説明するのって結構難しいな。しかもこれを喋らなきゃいけないのか。うひー。日頃からこういうことやってる人って凄いな。

書いてて思ったんだけど、最近全然Objective-Cのコード書いてないな。Twitterクライアント作ってたけど、ちょうどその夏ライオンで満足してるし、iPhotoプラグイン開発は本業が忙しくなってから頓挫中だしなー。そろそろ何か作らないとな、と言いつつRubyに浮気してるし。うー。だってRubyかわいいんだもの。

ブログネタ
Objective-C に参加中!

詳解Objective-C 2.0読書会に参加してきた。読書会というのに参加したのは初めてだけど、皆で同じ本を読んで、読んだ部分に対して「ここがわからない」「いやそれは実はこういうことで…」というやりとりは中々面白かった。一人で読んでるとスルーしがちなところも拾ってみると勉強になるなぁ。

それより何より、Objective-Cなんていうマニアックな言語の、読書会なんていうストイックな催しに、24人も集まったのが凄い。これもiPhone効果かーなんて思ったけど、現時点ではiPhoneを買うと表明した人が少なかったのも印象深かった。いや俺も買わないけど。とは言えなんだかんだで衝動買いしかねないけど。どっちだよ。

今日はCHAPTER2とCHAPTER3の途中までを読んだ。CHAPTER2はObjective-Cの基本的な特徴や構文、CHAPTER3がクラス定義と継承だった。内容に関しては詳解Objective-C 2.0を読んでもらうとして、以下は読書会中に挙がった話の補足。

[[Hoge alloc] init]について

ObjCでクラスからオブジェクトを作る場合のイディオムについての話。「allocの後に続けて初期化処理を書かないといけないと言われたことがあるが、それは何故か、どう危険なのか、またそれが必須ならなぜ一纏めにしないのか」との質問が挙がった。

allocの後にinitもしくはinit...で始まるメッセージを続ける理由は、呼び出し側でそれを分けて使うことがまず無いから。別に、

id obj = [Hoge alloc];
/* なんか別な処理 */
[obj init];

としたところで文法上は問題無いんだけど、NSObjectのクラスメソッドであるところのallocは単にメモリを割り当ててオブジェクトを生成しただけで、それがオブジェクトとして振る舞うためには最初にNSObjectのインスタンスメソッドのinitが呼び出されていなければならず、上のコードの変数objは「なんか別な処理」をしている最中には、「何もできないか、何かさせようとすると異常な挙動をする」。詳しくは見てないけど、NSObjectのinitは結構色々やってるはず。そんな不安定な状態のものを放っておくメリットはよっぽど特殊なケースでないと皆無なので、allocと初期化処理は同時にやってしまうようにしましょうと言う話。

じゃあ何でメモリ割り当てと初期化が別なんだ、一緒でいいだろ、と言われるとちょっと勉強不足で即答できないんだけど、内部的には未初期化のオブジェクトが効果的に使われてるところがあるのかもしれない。そういう設計思想だからじゃないか、としかわかんないなー。

ちなみに、「allocしてinitしたオブジェクトを返すクラスメソッド」はある。NSObjectにnewってメソッドが定義されてるので、上記のイディオムの代わりに、

id obj = [Hoge new];

って書いても同じ意味になる。ただこれだと引数なしのinitを呼び出すだけなので、引数付きで同等のことをやりたかったらそういうことをするクラスメソッドを定義してやる必要がある。あと、Cocoaのクラスでも初期化で色々ややこしいことしてるやつは、そういうクラスメソッドが用意されている。例えばNSArrayのarrayWithObjects:とか。ちなみに、Fooクラスのクラスメソッドの「fooWithHoge:」と、Fooクラスのインスタンスメソッドの「initWithHoge:」は対応させるのが慣習になっているようだ。

初期化処理のイディオム

詳解Objective-C 2.0には、初期化処理は通常次のようにする、と書かれている。

- (id)init
{
    self = [super init];
    if (self != nil)
    {
        /* 何らかの初期化処理 */
    }
    return self;
}

これは実は、次のように書いても大抵の場合問題ない。

- (id)init
{
    [super init];
    /* 何らかの初期化処理 */
    return self;
}

スーパークラスの初期化処理を明示的に呼び出すことと、returnを省略できないのは変わらないんだけど、明示的なselfへの代入とnilチェックはしなくても処理できる。スーパークラスの初期化処理を辿ってNSObjectのinitが呼び出されると自動的にselfへオブジェクトが代入されるし、仮にselfがnilでも「nilに対するメッセージ送信は単に無視される」。まぁインスタンス変数の扱いとかは危ういけど。

じゃあ何でわざわざselfへの代入を明示的にするのかと言えば、「selfに自分自身じゃないオブジェクトを代入してもいい」上に「スーパークラスのinitの返り値が自分自身とは限らない」から。そういうトリッキーな実装も可能なので(例えばNSStringとかは実は結構トリッキーなことをやってる。その話が出てくるのは大分先だけど)、何か特殊なことをするのでない時は上のイディオムで書いた方がいい。てかselfが自分自身じゃなくてもいいって凄いな。お前は誰だ。

superの意味

selfが単にオブジェクトだったのに対し、superは変数に代入したり付け替えたりはできない。多分予約語なんだよな。superは何かと言うと、「自分より上位の継承関係を辿って行って最初に見つけたクラスの実装を使って、自分のコンテキストでその処理を実行する」という構文。難しいね。

基本的には一つ上のスーパークラスを見て、なければさらに上へ…を繰り返して最終的にはNSObjectまで行く。ここで「一つ上からじゃなくて、スキップしていくつか上のクラスのメソッドを呼び出せるか」という話で盛り上がった。聞いて驚いたんだけど、実はすごくごちゃくごちゃしたコードを書けばできるらしい。けど、実行時に既存のクラスの書き換えまでやってのけるObjective-Cにおいて、コード上の継承関係を元にややこしい処理を行なうのは危険すぎる。最終的には「できるかできないかより、そんなことをしなきゃならないような設計の方が問題だよね」というオチがついた。そりゃそうだ。

メッセージセレクタ

「メソッドを呼び出す際に引数をつけるときには最後に:を付ける、複数の引数があるときは"keyword:"を並べる」というのを読んで、「第二引数以降にキーワードを付けるのは分かったけど、第一引数の前にはメソッド名があるだけでキーワードが無いけど、付ける方法はないのか」というの質問が上がった。ここはちょっと誤解しやすいポイントかも。多分、メソッド名とかメッセージキーワードとかいう語彙が悪い。

例えば、

[obj setValue:@"hoge" forKey:@"name"];

こういうときの「メッセージセレクタ」は「setValue:forKey:」のひとまとまりであって、その際に:や後ろのキーワードも含む。ちょっと無理矢理にRuby風に書くと、以下のようなイメージ。

# Rubyではメソッド名に:は使えないけど、仮に使えるとしたら
obj.setValue:forKey:("hoge", "name")
# もっと言うと、こういうイメージかも
# この方が「メッセージパッシング」っぽいし、
# 実際こういうメソッド呼び出しの方法はObjCにもある、
# というか内部的にはCの関数でこういうことをしている
obj.__send__(:setValue:forKey:, "hoge", "name")

なので、「最初の一個目がメソッド名で、二個目以降がキーワード」というのは誤り。一見分かれてるようだけど「全部くっつけたもの = メソッド名」だと思った方がいいし、セレクタ(SEL)を使って上のRubyの__send__みたいなことをやるときは正に、

SEL sel = @selector(setValue:forKey:);

などとして「メソッド名を生成」する。

ちなみに、最初の「一個目の引数の説明はどこに書くのか」に対しては、「何の引数を取るのかわかるメソッド名にする」が一応答えかな。Cocoaだと一個の引数を取るメソッドは例えば、

id url = [NSURL URLWithString:@"http://www.apple.com"];

のように、何の引数を取るか分かるように命名するのが慣習になっている。さっきのsetValue:forKey:も、「最初が値になるオブジェクトで、次がキーになるオブジェクト」ってのが分かるようになってるね。

ところで、ちょっと違う話になるけど、

# これはさっきの偽Ruby
obj.setValue:forKey:("hoge", "name")

これ何か見覚えあるなぁと思ったら、

# これはRubyCocoa
obj.setValue_forKey("hoge", "name")

RubyCocoaでOSX::NSObjectを継承したクラスのメソッドを呼び出すときとほぼ一緒(まぁ、そういう風に作ってあるのだけども)。RubyCocoaからMacアプリの世界に入った人は案外、ObjCのメソッド送信がすんなり分かるかもしれない。でもそれ以前にCocoaを知らないとRubyCocoaは分かりづらいって話はあるけど。ジレンマ。

詳解Objective-C 2.0読書会の今後

今日はとりあえず「音読して、途中途中で質疑応答タイムを設ける」っていう形式だったけど、今後は色々試行錯誤していくとのこと。あと、今回は休日だったけど、2時間程度であれば別に平日でも良いのでは?との意見から次回は試しに平日にしてみることになった。多分今月中にもう一回ぐらい開催されることになりそうなので、興味ある人はメーリングリストに参加してみるといいよ!

↑このページのトップヘ