NSSetとライフゲーム(その7、XLifeデータの読み込み) [プログラミング - NSSetとライフゲーム]
ライフゲーム、今更の内容だけど続いてるでぇ。
今日は初期データの読み込みについて。特に「なんちゃってクラスクラスタ」の実装のこと。
さて、ライフゲームには初期データを与えなければいけない。そのための専用エディタを備えるのがスジだとは思うが、それは大変。今回の目的(NSSetの特徴を生かしてFペントミノの最後を見届ける)のためにそこまでする必要は無い。そこで巷にころがるデータファイルを読み込めるようにしよう。そうすれば人の書いてくれたエディタが使えるし、テキストエディタでもいいし。
ということでまず、超有名なXlife(どこがオリジナルかよくわからないけどたとえばここ)のデータを読み込めるようにする。Xlifeはちょっと古いけどX-windowで動くライフゲームで、エディタを含めた必要な機能がすべて揃った上にConwayのオリジナルの遷移規則以外も定義できたり、今回やってるのと同じような実質的に無限の広さをもつ領域を扱える。Xlifeも同じような考え方(生きてるセルだけ保持する方法)で作られていると思われるが、ソースを見ても良くわからない。なにせCでX-WindowなのでObjective-CとCocoa frameworkに比べたらずっとたくさんのコードが書き連ねられている。ソースを開くと白いけどね。
#コメント 中身は意味があるように見えるけど良くわからない .** **. .*.
のような形(これはFペントミノのデータ)で文字を並べて「*」のあるところが生きているセルを表しているようだ。これを読み込めるようにする。他のフォーマットも後から追加できるようにしておこう。
Xlifeはいい加減なのかなんなのか、ちゃんとREADMEとか読めばちゃんと書いてあるのか知らないけど*.lファイルに違う形式のものが混じっている。それは
#コメント やっぱりなにか意味があるらしい x座標 y座標 x座標 y座標 ...
みたいな形式。これも読めるようにしておこう。でも同じ*.lファイルでは読み込んでから中身を解析する必要がある(「#」から始まるコメント行を見ればわかるらしいけど)がそれは避けたい(if文の連鎖やcase文の中身がぶりぶり膨らむするのはいや)。拡張子を見て振り分けることにしよう。形式の違うファイルは拡張子を書き換えよう。面倒だけどコードが面倒になるよりマシということで。
そのためにはクラスクラスタのような実装が追加に対応しやすい。
CocoaのFoundation frameworkにはクラスクラスタがいっぱいある。たとえばNSStringはNSCFString、NSConstantString、NSPathStore、etc...などなど一体いくつあるのかわからないほど実体になるサブクラスがあって、その窓口がNSStringになっている。普通に
[NSString alloc]
で本体を作ったつもりでもallocで返されるのはダミーのいわゆるシングルトンでそのあとのinit...のメソッドで実体が実際に作られるらしい。この辺の話は
「ダイナミックObjective-C」に詳しいけどなかなかディープな話のようでちょとしんどい。
今回は「なんちゃってクラスクラスタ」にしよう。つまり、まず入り口になるパブリッククラスGoLReadDataFileを作る。
@interface GoLReadDataFile : NSObject { NSString *path; } + (BOOL)isLoaded; + (void)registerSubclass:(Class)subclass withExtension:(NSString *)ext; + (NSArray *)supportedFileTypes; - (id)initWithPath:(NSString *)filePath; - (NSArray *)parseFile; @end
後からフォーマットごとにサブクラスとして追加する。後から追加してもパブリッククラスを変更しなくてもいいような仕組みを作っておく。
自分の親クラスはsuperで指定できるし何のクラスか実行時でもわかる。ところが自分のサブクラスがあるかどうか知る手段は(少なくともObjective-Cのレベルでは)ない。サブクラスを追加するたびに親クラスを書き換える(case文の分岐をサブクラスごとに書いたりする)こともしたくない。
そこでGoLReadDataFileのサブクラスはこの親クラスに自分を「登録」することにする。それがregisterSubclass:withExtension:メソッドである。サブクラスは親クラスのこのメソッドを使って自分自身のクラスとサポートする拡張子を登録する。そして作業実体としてサブクラスはinitWithPath:とparseFileのメソッドをoverrideする。
親クラスは自分のインスタンスが作られてデータファイルのパスが渡されたとき、その拡張子を見て登録されているか調べて、されていたらそれをサポートするサブクラスのインスタンスを作ってpathを渡し、自分自身をreleaseする。この辺(自分自身をreleaseする)のあたりが「なんちゃって」のゆえんで、実は親クラスのインスタンスは必要ないし、コストの高いインスタンス生成を必ず伴う実装はよろしくない。だからNSStringのallocメソッドはシングルトンを返すわけだけどそれをやりだすと大変なのでなんちゃってですますことにする。手間が減ってわかりやすくなればそれでよい。NSStringと違って頻繁に呼ばれる訳ではないし。
なんちゃってはいいとして問題なのは「登録」のタイミングである。登録はまだインスタンスが存在していないときにクラスオブジェクトが作業をしなければならない。クラスオブジェクトの初期化用にNSObjectの二つのクラスメソッドがある。
+(void)load; +(void)initialize;
loadメソッドはObjective-Cのランタイムがクラスオブジェクトを作るときにそれぞれのクラスオブジェクトに投げられる。initializeメソッドの方はクラスが初めて使われるときに、その使われる直前に呼ばれる。initializeメソッドは継承関係にあるクラスを親クラスから順に呼ぶことが保証されている(サブクラスにinitializeメソッドが無いともういちど親が呼ばれるので親はinitializeが複数回呼ばれてもいいように書いておけとrefernceに書いてある)。loadメソッドはinitializeメソッドに先立ち、最も早く呼ばれるが順番が保証されないのでreferenceにはなるべくinitializeメソッドを使うように書いてある。
initializeはさっきも書いたが初めてそのクラスが使われるときにその直前に呼ばれる。今回の場合、ファイルを開くOpenDialogを閉じた後にGoLReadDataFileが初めて使われる。当然その時点でGoLReadDataFileのサブクラスはお呼びでない。従って登録はされていない。
というわけで、今回は残念ながらinitializeメソッドで登録する方法を思いつかなかったのでloadを使うことにした。登録にはNSMutableArrayを使うのでloadが呼ばれた時点でNSMutableArrayのクラスが存在していないといけない。ランタイムまで中身を見るのは骨が折れるので(mkinoさんに任そう)とりあえずNSMutableArrayは既に存在している(あまりに基本的なクラスだからねえ)としてGoLReadDataFileにisLoadedというメソッドを設け、サブクラスは自分のloadが呼ばれたとき先に親クラスのloadが呼ばれたかチェックし、呼ばれていなければ呼ぶ、ということにする。あまり美しくないが、とりあえず動くので目をつぶることにする。
これでXlife以外のファイルもあとから読み込むことができるようになる(する気があれば、だけど)。
コメント 0