Skip to content

KVC の拡張:括弧付きキーパス

2010.10.10

これまでの記事で、KVC (Key-Value Coding) によるオブジェクトの取得についていろいろと見てきました。NSSet と NSArray の特別な挙動や、 NSDictionary の特別な挙動Set And Array Operators をマスターすることで、取得できるオブジェクトの幅が広がりました。しかし、デフォルトの KVC には弱点があります。

今回は、その弱点を明らかにし、valueForKeyPath: メソッドを拡張することで、その弱点を克服してしまいます。


デフォルトの KVC には弱点があります。まずは、その弱点を明らかにしたいと思います。

例えば、次のような NSDictionary 型変数 bowling があるとします。

bowling = {
    players = (
        {
            name = A;
            scores = (
                120,
                150,
                180
            );
        },
        {
            name = B;
            scores = (
                170,
                190,
                120
            );
        },
        {
            name = C;
            scores = (
                50,
                250
            );
        }
    );
}

このとき、平均スコアは、(120 + 150 + 180 + 170 + 190 + 120 + 50 + 250) / 8 = 153.75 となりますが、この値を bowling をレシーバとした valueForKeyPath: で求められるでしょうか。求められません。
次のように、valueForKeyPath: を2回行えば、求めることができるのですが。

NSNumber *average;
average = [[bowling
    valueForKeyPath:@"players.@unionOfArrays.scores"]
    valueForKeyPath:@"@avg.self"];
// average = 153.75

また、bowling に於いて、ユニークなスコアの数を bowling をレシーバとした valueForKeyPath: で求められるでしょうか。求められません。
次のように、valueForKeyPath: を2回行えば、求めることができるのですが。

NSNumber *scoreVariationCount;
scoreVariationCount = [[bowling
    valueForKeyPath:@"players.@distinctUnionOfArrays.scores"]
    valueForKeyPath:@"@count"];
// scoreVariationCount = 7

ふたつの問題は、どちらも原因は同じ。「valueForKeyPath: で得られるオブジェクトに対して valueForKeyPath: を行える、そんなキーパスを定義することができない」からです。これが、デフォルトの KVC の弱点です。

今回は、valueForKeyPath: を拡張することで、この弱点を克服してしまいます。「valueForKeyPath: で得られるオブジェクトに対して valueForKeyPath: を行える、そんなキーパスを定義することができる」ようにします。

拡張後の valueForKeyPath: は、以下のような括弧付きキーパスを解釈するようにします。

(keyPathThatReturnsResult).keyPathAppliedToResult

valueForKeyPath: が呼ばれたオブジェクトは、まず、括弧に囲まれた keyPathThatReturnsResult を引数に valueForKeyPath: を実行し、結果オブジェクトを得ます。次に、その結果オブジェクトをレシーバ、keyPathAppliedToResult を引数に valueForKeyPath: を実行します。これによって得られたオブジェクトを、元々の valueForKeyPath: の結果として返します。

valueForKeyPath: の拡張は以下のようにして行いました。
* keyPathThatReturnsResult と keyPathAppliedToResult が更に括弧付きのキーパスでも動くようにしてあります

<main.m>

#import <Cocoa/Cocoa.h>
#import <objc/runtime.h>


@interface NSObject (RLRExtension)
- (id)x_valueForKeyPath:(NSString*)keyPath;
@end

@implementation NSObject (RLRExtension)
- (id)x_valueForKeyPath:(NSString*)keyPath
{
    if ([keyPath hasPrefix:@"("] && [keyPath length] > 1) {
        // Get closingRange
        NSRange closingRange = {NSNotFound, 0};
        NSRange range = {1, 1};
        NSString *substring;
        NSInteger ignoreCount = 0;
        NSUInteger length;
        length = [keyPath length];
        while (range.location < length) {
            // Get substring
            substring = [keyPath substringWithRange:range];
            
            // Check substring
            if ([substring isEqualToString:@")"]) {
                if (ignoreCount) {
                    ignoreCount--;
                }
                else {
                    closingRange = range;
                    break;
                }
            }
            else if ([substring isEqualToString:@"("]) {
                ignoreCount++;
            }
            
            // Increase range.location
            range.location++;
        }
        if (closingRange.location == NSNotFound) {
            // Wrong format
            goto SUPER;
        }
        
        // Get keyPathAppliedToResult
        NSString *keyPathAppliedToResult;
        keyPathAppliedToResult = [keyPath substringFromIndex:NSMaxRange(closingRange)];
        if ([keyPathAppliedToResult length]) {
            if (![keyPathAppliedToResult hasPrefix:@"."]) {
                // Wrong format
                goto SUPER;
            }
            keyPathAppliedToResult = [keyPathAppliedToResult substringFromIndex:1];
        }
        
        // Get keyPathThatReturnsResult
        NSString *keyPathThatReturnsResult;
        NSRange sorroundedRange;
        sorroundedRange.length = NSMaxRange(closingRange) - 2;
        sorroundedRange.location = 1;
        keyPathThatReturnsResult = [keyPath substringWithRange:sorroundedRange];
        
        // Execute keyPathThatReturnsResult
        id result;
        result = [self valueForKeyPath:keyPathThatReturnsResult];
        
        // Execute keyPathAppliedToResult
        if ([keyPathAppliedToResult length]) {
            result = [result valueForKeyPath:keyPathAppliedToResult];
        }
        
        return result;
    }
    
SUPER: {
    // Super
    return [self x_valueForKeyPath:keyPath];
}
}
@end

#pragma mark -


int main(int argc, char *argv[])
{
    // Swap -[NSObject valueForKeyPath:]
    Method originalMethod;
    Method newMethod;
    originalMethod = class_getInstanceMethod([NSObject class], @selector(valueForKeyPath:));
    newMethod = class_getInstanceMethod([NSObject class], @selector(x_valueForKeyPath:));
    method_exchangeImplementations(originalMethod, newMethod);
    
    return NSApplicationMain(argc,  (const char **) argv);
}

これで、valueForKeyPath: が括弧付きのキーパスを解釈するようになりました。それでは、bowling の平均スコアを求めてみましょう。

問:平均スコアを求めよ

NSNumber *average;
average = [bowling valueForKeyPath:
    @"(players.@unionOfArrays.scores).@avg.self"];
// average = 153.75

できました。

次に、ユニークなスコアの数を求めてみましょう。
問:ユニークなスコアの数を求めよ

NSNumber *scoreVariationCount;
scoreVariationCount = [bowling valueForKeyPath:
    @"(players.@distinctUnionOfArrays.scores).@count"];
// scoreVariationCount = 7

できました。

また、KVC: Set And Array Operators 複数使いでいくつかの問に答えましたが、括弧付きキーパスを使った方が「何をしているのか分かりやすい」コードになる場合があります。(好みの問題もあると思います)

問:最高得点を求めよ

NSNumber *maxScore;
maxScore = [bowling valueForKeyPath:
    @"(players.@unionOfArrays.scores).@max.self"];
// maxScore = 250

問:player A, B, C の総得点を求めよ

NSNumber *sumOfScores;
sumOfScores = [bowling valueForKeyPath:
    @"(players.@unionOfArrays.scores).@sum.self"];
// sumOfScores = 1230

問:総ゲーム数を求めよ

NSNumber *allGameCount;
allGameCount = [bowling valueForKeyPath:
    @"(players.@unionOfArrays.scores).@count"];
// allGameCount = 8

括弧付きキーパス、如何だったでしょうか。valueForKeyPath: を拡張することで、デフォルトの KVC ではできないことができるようになりました。また、好みの問題はありますが、感覚的ではなかったキーパスが感覚的になりました。

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