24/7 twenty-four seven

iOS/OS X application programing topics.

NSArrayやNSDictionaryからNSNullを効率よく取り除く

iOSアプリケーションでWeb APIから返ってきたJSONを処理するのにNSNullの扱いに困っていて、事前にNSNullを取り除いてしまうのが事故を防ぐための確実な方法なのですが、再帰的にすべての要素を検査する以外になにかいい方法がないかと思って考えていたらちょっとおもしろい方法を思いついたので書いてみました。


kishikawakatsumi/CollectionUtils · GitHub

↑ に含まれるCUCompactArrayとCUCompactDictionaryです。


NSArrayとNSDictionaryのサブクラスとして実装されていて、次のようにして生成します。
(普通にalloc/initを使って生成することも可能です)

NSArray *array = @[@"0", @"1", [NSNull null], @"2", [NSNull null], @"3"];
NSArray *compactArray = [array cu_compactArray];
//=> ["0", "1", "2", "3"]
NSDictionary *dictionary = @{@"one": @"1",
                             @"null": [NSNull null],
                             @"two": @"2",
                             @"three": @"3"};
NSDictionary *compactDictionary = [dictionary cu_compactDictionary];
 //=> {"one": "1", "two": "2", "three": "3"}


JSONオブジェクトに対して使われることを想定しているので、ネストしたコレクションに対しても有効です。

NSArray *array = @[@"0",
                   @"1",
                   [NSNull null],
                   @"2",
                   @{@"one": @"1",
                     @"null": [NSNull null],
                     @"two": @"2",
                     @"three": @"3"},
                   @"4"];
NSMutableArray *compactArray = [array cu_compactArray];
//=> ["0", "1", "2", {"one": "1", "two": "2", "three": "3"}, "4"]


動作の仕組みは、まず与えられたNSArrayかNSDictionaryの最初の階層についてだけ、NSNullをチェックして取り除きます。
ここで、チェックされるのはあくまでもネストした最初の階層についてだけです。

- (instancetype)initWithObjects:(const id [])objects count:(NSUInteger)cnt
{
    self = [super init];
    if (self) {
        NSNull *nul = [NSNull null];
        NSMutableArray *mutableArray = [[NSMutableArray alloc] initWithCapacity:cnt];
        for (NSUInteger i = 0; i < cnt; i++) {
            id object = objects[i];
            if (object && object != nul) {
                [mutableArray addObject:object];
            }
        }
        _original = mutableArray;
        
    }
    return self;
}


そして、元のNSArrayかNSDictionaryを保持して同様の振る舞いをしつつ、実際にネストしたNSArray/NSDictionaryにアクセスがあったときに、適宜同じようにNSNullを削除したラッパーを返すことで、最終的にすべてのネストされた要素からNSNullが取り除かれるという結果になります。

#pragma mark - primitive instance methods

- (NSUInteger)count
{
    return self.original.count;
}
- (id)objectAtIndex:(NSUInteger)index
{
    id object = self.original[index];
    if ([object isKindOfClass:[CUCompactArray class]] || [object isKindOfClass:[CUCompactDictionary class]]) {
        return object;
    }
    if ([object isKindOfClass:[NSArray class]]) {
        return [CUCompactArray arrayWithArray:object];
    }
    if ([object isKindOfClass:[NSDictionary class]]) {
        return [CUCompactDictionary dictionaryWithDictionary:object];
    }
    return object;
}


このようにすることで、最初にすべての要素をチェックする方法だとNSNullがたとえ1つも含まれていなかったり、その値にアクセスすることがなくても、同じだけ処理時間がかかっていたのですが、実際にアクセスのあったタイミングで必要なところだけ処理することで、パフォーマンスに対するペナルティは非常に小さくなります。

けっこうおもしろいアイデアかなと思っているのですがいかがでしょうか?