Skip to content

KVC: Set And Array Operators @distinctUnionOfObjects

2010.08.30

KVC (Key-Value Coding) の Set And Array Operators をマスターすると、コードを簡素化できたり、NSPredicate でできることの幅が増えたり、Cocoa Bindings でできることの幅が増えるなどの利点があります。Set And Array Operators の概要から始まったシリーズ。

今回は、@distinctUnionOfObjects オペレータを取り上げます。


@distinctUnionOfObjects オペレータは、オブジェクトを重複のないように収集し、配列として返すオペレータです。基本的には前回取り上げた @unionOfObjects オペレータと同じですが、重複がない配列を返すところが異なります。

前回は、次のような NSDictionary 型変数 bowling から、player A, B, C それぞれの平均スコアを配列として取得しました。

bowling = {
    players = (
        {
            name = A;
            scores = (
                120,
                150,
                180
            );
        },
        {
            name = B;
            scores = (
                170,
                190,
                120
            );
        },
        {
            name = C;
            scores = (
                50,
                250
            );
        }
    );
}
NSArray *averages;
averages = [bowling valueForKeyPath:
    @"players.@unionOfObjects.scores.@avg.self"];

上の例で、averages は (150, 160, 150) という配列になりました。

@distinctUnionOfObjects オペレータを使うと、重複したオブジェクトが含まれなくなります。

NSArray *distinctAverages;
distinctAverages = [bowling valueForKeyPath:
    @"players.@distinctUnionOfObjects.scores.@avg.self"];

上の例で、distinctAverages は (150, 160) という配列になります。

@distinctUnionOfObjects を使わずに distinctAverages を求める場合、コードは次のようになります。

NSArray *distinctAverages;
NSMutableSet *averageSet;
NSArray *players;
NSNumber *average;
players = [bowling valueForKey:@"players"];
averageSet = [NSMutableSet set];
for (NSDictionary *player in players) {
    average = [player valueForKeyPath:@"scores.@avg.self"];
    if (average) {
        [averageSet addObject:average];
    }
}
distinctAverages = [averageSet allObjects];

@distinctUnionOfObjects オペレータを使用した方が、簡潔に結果を求めることができます。

パフォーマンスはどうでしょうか。比較してみました。
比較には以下のコードを使用しました。使用した機種は MacBook Pro。Mac アプリとして作成し、実行しました。

@implementation distinctUnionOfObjectsAppDelegate

- (NSDictionary*)bowling
{
    // Make bowling
    NSDictionary *bowling;
    NSMutableArray *players;
    players = [NSMutableArray array];
    bowling = [NSDictionary dictionaryWithObjectsAndKeys:
        players, @"players",
        nil];
    
    // Add player to players
    NSDictionary *player;
    NSArray *scores;
    for (NSInteger i = 0; i < 1000; i++) {
        // Make scores
        scores = [NSArray arrayWithObjects:
            [NSNumber numberWithUnsignedInt:arc4random()],
            [NSNumber numberWithUnsignedInt:arc4random()],
            [NSNumber numberWithUnsignedInt:arc4random()],
            nil];
        
        // Make player
        player = [NSDictionary dictionaryWithObjectsAndKeys:
            [NSString stringWithFormat:@"Name%ld", i], @"name",
            scores, @"scores",
            nil];
        
        // Add player to players
        [players addObject:player];
    }
    
    return bowling;
}

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    CFAbsoluteTime startTime;
    CFTimeInterval sumOfTime;
    NSUInteger const count = 100;
    
    // Get bowling
    NSDictionary *bowling;
    bowling = [self bowling];
    
    // @distinctUnionOfObjects version
    sumOfTime = 0.0;
    for (int i = 0; i < count; i++) {
        // Record startTime
        startTime = CFAbsoluteTimeGetCurrent();
        
        // Get distinctAverate
        NSArray *distinctAverages;
        distinctAverages = [bowling valueForKeyPath:
            @"players.@distinctUnionOfObjects.scores.@avg.self"];
        
        // Update sumOfTime
        sumOfTime += (CFAbsoluteTimeGetCurrent() - startTime);
    }
    NSLog(@"time average = %f", sumOfTime / (double)count);
    
    // Manual version
    sumOfTime = 0.0;
    for (int i = 0; i < count; i++) {
        // Record startTime
        startTime = CFAbsoluteTimeGetCurrent();
        
        // Get distinctAverages
        NSArray *distinctAverages;
        NSMutableSet *averageSet;
        NSArray *players;
        NSNumber *average;
        players = [bowling valueForKey:@"players"];
        averageSet = [NSMutableSet set];
        for (NSDictionary *player in players) {
            average = [player valueForKeyPath:@"scores.@avg.self"];
            if (average) {
                [averageSet addObject:average];
            }
        }
        distinctAverages = [averageSet allObjects];
        
        // Update sumOfTime
        sumOfTime += (CFAbsoluteTimeGetCurrent() - startTime);
    }
    NSLog(@"time average = %f", sumOfTime / (double)count);
}

@end

測定結果は以下の通りになりました:

@distinctUnionOfObjects version 0.027977 s
Manual version 0.027962 s

@distinctUnionOfObjects オペレータを使用した方が速度が若干遅くなる、という結果が出ました。@distinctUnionOfObjects を使用するべきかどうかは、ケース・バイ・ケースということになりそうです。

さて、開発を続けていると、「配列を持っているが、そこから重複分が削除されたオブジェクトの集合が欲しい」という状況があります。

例えば、次のような models 配列があるときです。

models = (
    "iPhone 4",
    "iPad Wi-Fi",
    "iPhone 3GS",
    "iPhone 3G",
    "iPod touch 1st",
    "iPhone 4",
    "iPad 3G",
    "iPod touch 2nd",
    "iPad Wi-Fi",
    "iPod touch 3rd",
    "iPhone 4",
    "iPad Wi-Fi",
    "iPod touch 1st",
    "iPhone 3G",
    "iPad 3G",
    "iPhone 3G",
    "iPad Wi-Fi",
    "iPhone 3GS",
    "iPod touch 3rd",
    "iPad 3G",
    "iPod touch 1st"
)

この models 配列から、重複分が削除されたオブジェクトの集合が欲しい場合、@distinctUnionOfObjects オペレータを使用して、次のように求めることができます。

NSArray *distinctModels;
distinctModels = [models valueForKeyPath:
    @"@distinctUnionOfObjects.self"];

上の例で、distinctModels は以下のようになります。

distinctModels = (
    "iPod touch 3rd",
    "iPhone 3GS",
    "iPod touch 2nd",
    "iPad Wi-Fi",
    "iPhone 4",
    "iPod touch 1st",
    "iPhone 3G",
    "iPad 3G"
)

また、@distinctUnionOfObjects オペレータを使わずに、以下のようにしても求めることができます。

NSSet *distinctModels;
distinctModels = [NSSet setWithArray:models];

パフォーマンスはどうでしょうか。比較してみました。

@distinctUnionOfObjects を使用 0.000076 s
NSSet に変換 0.000005 s

NSSet に変換するだけの方が10倍以上速いという結果になりました。配列から重複分を削除したいときは、NSSet に変換した方がよさそうです。

次回は、@unionOfArrays を取り上げます。

Advertisements

From → Develop

Leave a Comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s