Objective-Cという言語が面白いのは、コンパイラ言語でありながら、インタプリタ言語のような動的操作ができる点ではないかと思う。
実際にはCの上に構築されたプリプロセッサのようなイメージで、その操作はCのコードとランタイムライブラリへのアクセスへと還元することが出来る。そしてCの部分は隠蔽されることなく使用することが出来、それ故、インラインアセンブラのようにObjective-Cの操作をCで記述したコードを混在させるといったことも出来てしまう。そういった泥臭さはC言語族の特徴と言うべきなのだろうか。
ランタイムライブラリをいじらないまでも、例えば文字列で指定されたクラス名のインスタンスを作成するのはこんな感じで出来る。
Class klass = NSClassFromString(className); id obj = [klass alloc];
この機能を使用すると、テキストファイルで指定されているクラスのインスタンスを動的に作成するというようなことが出来る。
さらに、標準のXMLパーサNSXMLParserと組み合わせると、手っ取り早く(とりあえずの)設定を読み取り、インスタンスを生成するモジュールを実装することが出来る。
仕込みとしては、生成したいクラスにinitWithAttributes:という初期化メソッドを実装しておく。Objective-Cはduck typing対応(?)なので、特に何かの基底クラスの派生クラスでないといけないという制約はない。
NSXMLParserはエレメントを見つけたときのdelegate呼び出しに、エレメントのアトリビュートをNSDictionaryで渡してくれる。これをそのままインスタンスの初期化に渡してしまおうというわけだ。
#define kAttributeName @"name" @implementation Foo - (id) initWithAttributes:(NSDictionary *)attributes { if ((self = [super init])) { self.name = [attributes objectForKey:kAttributeName]; } return self; } @end
NSXMLParserは、せめてuserData等のプロパティがあれば良かったのだけど、無いので、派生させて必要なプロパティを追加しておく。
@interface KTXMLParser : NSXMLParser @property (assign) NSUInteger state; @property (assign) NSMutableArray *objects; @end @implementation KTXMLParser @synthesize state = _state; @synthesize objects = _objects; @end
パース部分は相当手抜きして、ルートとその子エレメントしか考慮しないことにする。
- (void)parser:(KTXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict { switch (parser.state) { case 0: if (![elementName isEqualToString:kRootElement]) { NSLog(@"The document starts with unknown element %@",elementName); [parser abortParsing]; } break; case 1: { id obj = _createInstance(elementName, attributeDict,_cmd); if (obj) { [parser.objects addObject:obj]; [obj release]; } else { NSLog(@"Instance creation failure"); [parser abortParsing]; } } break; default: NSLog(@"Invalid document structure"); [parser abortParsing]; } parser.state += 1; }
エレメント名をクラス名としてインスタンスを作成する。<Foo …/>だとFooクラスのインスタンスと言うことになる。
そしてインスタンス作成部分はというと。
static id _createInstance(NSString *className,NSDictionary *attributes,SEL _cmd) { Class klass = NSClassFromString(className); id obj = nil; if (klass) { obj = [klass alloc]; if ([obj respondsToSelector:@selector(initWithAttributes:)]) { obj = [obj performSelector:@selector(initWithAttributes:) withObject:attributes]; } else { NSLog(@"A instance of class %@ can not respond to selector initWithAttributes:",className); [obj release]; obj = nil; } } else { NSLog(@"Unknown class:%@",className); } return obj; }
メソッドではなくstaticな関数として実装しているのは、Objective-Cにはプライベートメソッドが存在せず、そのかわりなのか、@implementaionと@endの中にある関数はインスタンスのプライベートメンバにアクセスが出来る。つまり、プライベートメソッドはそういう風に実装してねという暗黙の了解なのかもしれない。と、いった理由による。
引数で_cmdを渡しているのだけど、これはメソッドの中では自動的に定義される変数で、そのメソッドのセレクタを示す。NSLogは_cmdを必要とするので、とりあえず呼び出し元の値を渡している。
_createInstanceの中身の主なところは前出の指定クラスのインスタンスの作成だ。そのあと、initWithAttributes:が実装されているかを調べ、実装されている場合には呼び出し、されていない場合は削除してnil(失敗)を返す。
以上で主な実装は終了だ。クラスを追加したとしても、initWithAttributes:にさえ対応していれば特に追加で実装しなくても良い。つまり、使い回しがきく。
実際には作成できるオブジェクトの制限がないので、プロダクトに使うのはどうかと思うのだけど、プロトタイピングでサクッと試したい場合には便利だ。昨年作成したKinectのプロジェクトでも使用している。
ソースはhttps://gist.github.com/1577809を参照ください。