54. 永続化が必要なら,NSCoding Protocolを実装する

まともなApplicationを作ろうとするとDocument-BaseだUndoだNSCodingだと結構面倒くさいです.
けど,どれも上手いこと出来ているので,Cocoaな仕様から外れなければ楽ですが,Audio ApplicationだとAudio UnitのPresetの保存だなんだとCocoaじゃない部分がもりもりでそれなりに大変.

さて,久々更新,Effective? Objective-C.やっと8回目.

54. 永続化が必要なら,NSCoding Protocolを実装する
(J. Serializableを注意して実装する)

Javaのシリアライズがどんなだったか忘れましたが,ようはオブジェクトをファイルに書き出したり,ファイルから復元したりするということです.
Cocoaではどうなのか.
cocoa_breakさんが翻訳されているので(素晴らしい)参照してみると,

任意のオブジェクトをシリアル化することはできません。NSArray、NSDictionary、NSString、NSDate、NSNumber、NSData(とそれらのサブクラスのいくつか)のインスタンスだけがシリアル化できます。配列やディクショナリオブジェクトの内容は、また、これらの数少ないクラスのオブジェクトだけを含んでいなければなりません。

アーカイブをサポートするには、オブジェクトは NSCoding プロトコルを実装しなければなりません。これは2つのメソッドからなります。一方のメソッドは、オブジェクトの重要なインスタンス変数をアーカイブのなかにコード化 (encode) し、もう一方はアーカイブからインスタンス変数をコード復元 (decode) して元に戻します。

とあります.
NSCoding ProtocolはNSObject.hに定義されています.

@protocol NSCoding

- (void)encodeWithCoder:(NSCoder *)aCoder;
- (id)initWithCoder:(NSCoder *)aDecoder;

@end

この2つを実装すれば,シリアライズできるという事です.
どう実装するのか.

encodeWithCoderでは,NSCoderは,encode***というMethodを持つので,オブジェクトのインスタンスの型に合わせてMethodを呼んでやればOK.

encodeするインスタンスがint hogeというインスタンス変数を持つなら

[aCoder encodeInt:hoge forKey:@"hoge"];

これで,aCoderにencodeされたhogeが渡される.
initWithCoderでは,aDecoderからdecode***ForKeyで値を取り出してやればOK.

hoge = [aCoder decodeIntForKey:@"hoge"];

例のDavinci Classで実装してみます.
workOfArtだけだとつまらないのでIQというセンスゼロなインスタンス変数を追加してみた.

@interface Davinci : NSObject {
    NSString *workOfArt;
    double IQ;
}
@end

@implementation Davinci
- (void)encodeWithCoder:(NSCoder*)coder
{
    [coder encodeObject:workOfArt forKey:@"workOfArt"];
    [coder encodeDouble:IQ forKey:@"IQ"];
}
- (id)initWithCoder:(NSCoder*)decoder
{
    self = [super init];
    workOfArt = [decoder decodeObjectForKey:@"workOfArt"];
    [workOfArt retain];
    IQ = [decoder decodeDoubleForKey:@"IQ"];

    return self;
}

-(NSString*)description{
    return [NSString stringWithFormat:@"%@ :%p ,workOfArt:%@, IQ:%f>",[self className],self,workOfArt,IQ];
}
@end

これだけでシリアライズ/デシリアライズできる.

NSCoder Protocolを実装したオブジェクトはNSArchiver,NSKeyedArchiverでシリアライズ出来て,シリアライズしたデータは,NSUnarchiver,NSKeyedUnarchiverでデシリアライズ(復元)できる.

NSKeyedArchiverを使うとdecodeの時にデータを順番にdecodeしなくてもいいので主流らしい.

#import <Cocoa/Cocoa.h>
#import "Davinci.h"
int main(int argc, char *argv[])
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
    Davinci *davinci = [[Davinci alloc] init];
    [davinci setValue:@"Mona-Lisa" forKey:@"workOfArt"];
    [davinci setValue:[NSNumber numberWithDouble:250.123456] forKey:@"IQ"];
    NSLog(@" encodeDavinci = %@",davinci);

    NSData *data = [NSKeyedArchiver archivedDataWithRootObject:davinci];

    Davinci *decodedDavinci = [NSKeyedUnarchiver unarchiveObjectWithData:data];
    NSLog(@"decodedDavinci = %@",decodedDavinci);

    [pool release];
    return 0;
}

result….

 encodeDavinci = Davinci :0x306f10 ,workOfArt:Mona-Lisa, IQ:250.123456>
decodedDavinci = Davinci :0x30d520 ,workOfArt:Mona-Lisa, IQ:250.123456>

ということで,ちゃんと動きます.

Fileに書き出して,また読み込んでも

    NSString *path = @"/tmp/hoge";
    [NSKeyedArchiver archiveRootObject:davinci toFile:path];

    Davinci *decodedDavinci = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
    NSLog(@"decodedDavinci = %@",decodedDavinci);

ちゃんと動きます.

もしDavinci ClassがNSArray、NSDictionary、NSString、NSDate、NSNumber、NSData以外の他のClassをインスタンス変数に持っていたら,そのClassでもNSCoder Protocolを実装しないといけません.
何かをセーブしたいときに芋づる式にNSCoder Protocolをもりもり実装する羽目になるかもしれません.
しかしながら,これぐらいの手間で永続化出来るのはだいぶ便利な気がします.
ちなみにNSArray,NSDictionaryの場合,その持っているオブジェクトがNSCodingを実装していればそのままシリアライズ出来るので,DocumentのSave等で便利.
もう一つ.Objective-Cはインスタンス変数に何を持っているのか動的に取得出来るので,その型も取得しちゃえばencodeWithCoderもinitWithCoderも自動で実装出来ると思うのですが,なんで出来ないんでしょうか.
きっと出来るな.誰かやって.

というわけで,EffectiveというよりはNSCoderの初歩になってしまいましたが,
お題通り,永続化が必要なら,NSCoding Protocolを実装するでどうでしょうか.

NSCoding関係は拾いきれていない項目があるので,その内追加予定.

Effective? Objective-C 54.zip

One Comment

  1. sqlite 学習中。FMDB を知る。 said:

    [...] これまではデータ保存はもっぱら NSCoder APIだったが、今後はSQLも使ってみることができる。 。 [...]

Leave a Reply