2012年4月9日月曜日

NSUserDefaultsとmutable deep copy

NSUserDefaultsにmutableなオブジェクトを保存しても、取り出すとimmutableなオブジェクトが返る、とクラスリファレンスには書いてある。

Values returned from NSUserDefaults are immutable, even if you set a mutable object as the value. For example, if you set a mutable string as the value for "MyStringDefault", the string you later retrieve using stringForKey: will be immutable.

実際にはmutableなオブジェクトが返されることもあるようだけど、その挙動が保証されているわけではない。ということで、mutableなオブジェクトが必要な場合は-mutableCopyなどを使って作り直す必要がある。

NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
NSMutableArray* addresses = [[[userDefaults arrayForKey:@"Addresses"] mutableCopy] autorelease];

NSMutableArrayがさらにNSMutableDictionaryを含んでいるような場合、-mutableCopyはshallow copyなので使えない。-mutableCopyのdeep copy版があればいいのだけど、そんな便利なものはFoundationにはなくて、Core FoundationのCFPropertyListCreateDeepCopyという関数を使う。

NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
NSMutableArray* contacts = (NSMutableArray*)CFPropertyListCreateDeepCopy(NULL,
                                                                         (CFArrayRef)[userDefaults arrayForKey:@"Contacts"],
                                                                         kCFPropertyListMutableContainersAndLeaves);
[contacts autorelease];

返り値と第2引数(コピーするオブジェクト)の型はCFPropertyListRefで、これはプロパティリストオブジェクトの抽象型ということになっている。つまり、CFData, CFString, CFArray, CFDictionary, CFDate, CFBoolean, CFNumberのスーパークラスのようなもの。さらに、これらの型は対応するFoundationのクラスとtoll-free bridgedなので、結局、キャストするだけで対応するFoundationのクラスを渡したり受け取ったりできる。

第3引数ではどこまでmutableにするかを指定する。kCFPropertyListMutableContainersAndLeavesを指定すると、コンテナ(CFArrayCFDictionary)が含む末端のオブジェクトもすべてmutableになる。一方、kCFPropertyListMutableContainersの場合は末端のオブジェクトはimmutableになる。

CFPropertyListCreateDeepCopyは、NSUserDefaultsと関係なく汎用的に使うことももちろんできる。しかし、プロパティオブジェクトしかサポートしていないため、例えばNSSetなどが含まれているとエラーになってしまう。