Cocoa勉強会#51 発表 - clang trunkの新しいリテラル
clang trunkに導入された、@[obj1, obj2]でNSArrayになるなどの新しいリテラルとdict[key]なんかのアクセサ記法(なんていうのが正しい?)について。役に立つ情報というより雑談のネタ的に話しました。
発表の資料はこちら。
http://www.slideshare.net/kimuraw/objctivec-new-literal
ちゃんとした記事はCocoaの日々情報局の記事「最新 clang で @YESや dict[@"key"] が可能に」をごらんください。
以下内容の説明とか。時間の都合で勉強会では省略したネタも書いています。せっかく調べたんだし。
どこからでてきた話?
Xcodeのコンパイラとして利用されているclangはオープンソースのプロジェクトとして開発されているんだけど、最近こんなコミットがあった。r152137
Add clang support for new Objective-C literal syntax for NSDictionary,新しいリテラルが導入されたらしい。どんなんだろね。
NSArray, NSNumber, and boolean literals. This includes both Sema and
Codegen support.
Included is also support for new Objective-C container subscripting.
どんな書きかたができるか
中身のコード追っかけるより、テストがちゃんとあるのでそちらを見るのが簡単。今回は、test/SemaObjC (構文のテスト)や、test/ARCMT (新バージョンへの自動書き換え機能のテスト。XcodeのARC対応にする機能なんかでも使われてる)あたりがいいでしょう。
とくにtest/ARCMTは自動書き換えの前後のコードがテストの入力になるので、.mと.m.resultをdiffツールで見比べるとどう変わるのかがとてもわかりやすい。
たとえばFileMergeで見るとこうなる。
objcmt-subscripting-literals.mと.m.result
objcmt-numeric-literals.mと.m.result
例 - NSArray
@[ obj1, obj2 ] // [NSArray // arrayWithObjects:obj1, obj2, nil] ary[1] // [ary objectAtIndex:1] ary[1] = val // [ary replaceObjectAtIndex:1 withObject:val]
例 - NSDictionary
@[ key1 : val1, key2 : val2 ] // [NSDictionary // dictinaryWithObjectsAndKeys: // val1, key1, val2, key2, nil]
メソッドでは値 > キーの順、新しいリテラルではキー > 値の順になってる。どちらも意味的には自然だけどちょっとまぎらわしいね。
dict[key] // [dict objectForKey:key] dict[key] = val // [dict setObject:val forKey:key]
例 - NSNumber
@123 // [NSNumber numberWithInt:123] @-123 // [NSNumber numberWithInt:-123] @123.0 // [NSNumber numberWithFloat:123.0] @YES // [NSNumber numberWithBool:YES]
@YESはちょっとキモチワルイかも。従来のYESと@の有無で区別しろってのはキツイんじゃないかと思うなあ。型がちがうからコンパイラが警告出すとは思うけど。
obj[idx]のしくみ
ところで、NSArrayとNSDictionaryの新しいアクセサ記法って字面は一緒だよね。
obj = ary[1]; ary[1] = obj; // NSArray obj = dict[key]; dict[key] = obj; // NSDictionary
これって内部的にはどうやって見分けてるんだろ?考えてみた。
- 案1: インデックスによって見分けている。obj[idx]のidxが数値(NSInteger, int, long, ...)ならNSArrayとして、idxがオブジェクト(id型)ならNSDictionaryとして扱う。
- 案2: obj[idx]のobjクラスを判定してNSArrayおよびそのサブクラス、NSDictionaryおよびそのサブクラスかどうかによって判断する。
ようするにインデックスかレシーバのどっちかだよね、きっと。調べるにはどうしたらよいか。
- obj[idx]のobjをNSArrayやNSDictionary以外の型にして試してみる
- クラスで判定してるならそもそも通らない
- idxの型で判定してるならidxを数値、オブジェクトにした場合で変化があるはず
コード書いてコンパイル、動作確認してもいいんだけど、せっかくテストがあるのでそれを利用するのがよさそう。ということでターゲットはtest/SemaObjC/objc-array-literal.mにしよう。
Index: objc-array-literal.m =================================================================== --- objc-array-literal.m (revision 152917) +++ objc-array-literal.m (working copy) @@ -38,4 +38,8 @@ const char *blah; NSArray *array2 = @[blah]; // expected-error{{collection element of type 'const char *' is not an Objective-C object}} + + NSNumber *obj = @1; + NSLog(@"%@\n", obj[0]); + NSLog(@"%@\n", obj[@"key"]); }
ということで実行。事前にClang - Getting Startedの手順でtrunkをコンパイル、buildディレクトリで一度はmake testを通しているものとします。
% pwd .../build/tools/clang/test % make TESTDIRS=SemaObjC Making Clang 'lit.site.cfg' file... Making Clang 'Unit/lit.site.cfg' file... : -- Exit Code: 1 Command Output (stderr): -- error: 'error' diagnostics seen but not expected: Line 43: expected method to read array element not found on object of type 'NSNumber *' Line 44: expected method to read dictionary element not found on object of type 'NSNumber *' 2 errors generated. -- ******************** Testing Time: 25.28s ******************** Failing Tests (1): Clang :: SemaObjC/objc-array-literal.m Expected Passes : 357 Unexpected Failures: 1 make: *** [all] Error 1
ここ注目。
Line 43: expected method to read array element not found on object of type 'NSNumber *' Line 44: expected method to read dictionary element not found on object of type 'NSNumber *'
"expected method"ってことはメソッドがあればいいのか。
Index: objc-array-literal.m =================================================================== --- objc-array-literal.m (revision 152917) +++ objc-array-literal.m (working copy) @@ -21,6 +21,10 @@ @interface NSNumber + (NSNumber *)numberWithInt:(int)value; +- (id)objectAtIndexedSubscript:(NSUInteger)index; +// - (void)setObject:(id)object atIndexedSubscript:(NSUInteger)index; +- (id)objectForKeyedSubscript:(id)key; +// - (void)setObject:(id)object forKeyedSubscript:(id)key; @end @interface NSArray <NSFastEnumeration> @@ -38,4 +42,8 @@ const char *blah; NSArray *array2 = @[blah]; // expected-error{{collection element of type 'const char *' is not an Objective-C object}} + + NSNumber *obj = @1; + NSLog(@"%@\n", obj[0]); + NSLog(@"%@\n", obj[@"key"]); }
ということでもいちどテスト。
% make TESTDIRS=SemaObjC Making Clang 'lit.site.cfg' file... : Expected Passes : 358 %
うむ。
ということで、必要なメソッドさえ実装していれば obj[idx] や obj[idx] = val という書きかたが利用できるみたい。fast enumerationなんかと同じだね。(参考: ダイナミックObjective-C Fast Enumeration (4) - Fast Enumerationに対応するクラスの実装)
rubyはじめたころ、def self[](idx) にびっくりしたことを思い出したり。
(2012-03-20 追記)Big Nerd Ranch Weblogの記事"Objective-C Literals, part 2"の、Making your own indexable collectionsからのところにサンプルコード含め、詳しく書かれています。dict[key]での階層キー(Key Value CodingでいうところのvalueForKey:@"key1.key2.key3")でのアクセスなど。ぜひ読んでね。
ぼく個人の感想
RubyとかPythonに慣れてる人にとっては自然というか、従来の[NSArray arrayWithObjects:]とかがダルすぎるってとこでしょうか。ライブラリももちろんだけど、リテラルって言語のポイントとしてけっこー重要だよね。
dot syntaxなんかはgcc時代に導入されたものだけど、clangを手にしてからのAppleはすごく突っ走ってるよね。ARCとか。まー今回の件は妥当というか正しい方向だと思ってる。
あとちょっと気になるのが、従来のObjective-C/Cocoaのアプローチは
- オブジェクト、非オブジェクトの使い分け。たとえばintやfloatを多用。
- immutable, mutable
- NSArray, NSSet
というように細かく設計されてる傾向があるんだよね。たぶんパフォーマンスのためだと思うんだけど。これってリテラルの強化とちょっと相性がよくない(Rubyの「大クラス主義」のほうが向いてる)と思ってて、
- リテラルで定義したオブジェクトはmutableであるべき
- NSNumberと数値が直接比較できないのはがっくり
みたいな話がでてくるような気がします。次に書くMLでの反応ではすでにそんな傾向が見えてたり。
objc-language MLでの反応
objc-language MLのほうながめてたらこんな意見が。
- mutable Arrayのリテラルはないの? [@[elem1, elem2, ..] mutableCopy] とかダサいし。
- @[elem1, elem2, .. @]とかどう?
- NSSet, NSCoutedSetのリテラルもほしい!
- @()や@({})でどう?
いやおまえら、増やせばいいってもんじゃねーだろ。ややこしくなるじゃん。
勉強会での反応
- おおむね好評
- Objective-CはJavaScript化する?
- CoreDataには便利