Objective-C Key-Value Coding

Sunday, January 1st, 2012

Objective-Cには「Key-Value Coding (KVC)」というものが備わっており、文字列による名前指定でプロパティの値の取得、設定といったことができるらしい。Javaだと、Java Beans + BeanUtilsを組み合わせた機能といったところでしょうか。こういった機能が言語の標準機能として備わっているというのは面白い。
参考にした資料はKey-Value Coding Programming Guideです。

Objective-CにおけるKey-Value Coding

Key-Value Codingとは

オブジェクト指向言語であるObjective-Cでは、インスタンス変数へ直接アクセスすることは許されず、以下のようにsetter/getterを定義することとなります。

#import <Foundation/Foundation.h>

@interface MyClass : NSObject
{
    NSString *name;
}
-(NSString*)name;
-(void)setName:(NSString*)_name;
@end

@implementation MyClass
-(NSString*)name {
    return name;
}
-(void)setName:(NSString *)_name
{
    name = _name;
}
@end

int main (int argc, const char * argv[])
{
    @autoreleasepool {
        MyClass *obj = [[MyClass alloc]init];
        [obj setName:@"James"];
        NSLog(@"%@", [obj name]);
    }
    return 0;
}

当然ながら、setter/getterを自分で実装するのではなく、プロパティとして定義することも可能です。

@interface MyClass : NSObject
@property(strong) NSString *name;
@end

@implementation MyClass
@synthesize name;
@end

どちらにしてもnameというインスタンス変数にアクセスするために、メソッドnameとsetNameを使います。
ただ、特にGUIプログラミングでは、キー名を文字列で指定して、プロパティにアクセスできると相当のコード量を減らせる場合があります。それを実現するのがKey-Value Coding(KVC)です。
具体的には、「setValue: forKey:」で値を設定し、「valueForKey:」で値を取得します。以下がコード例です。

int main (int argc, const char * argv[])
{
    @autoreleasepool {
        MyClass *obj = [[MyClass alloc]init];
        [obj setValue:@"Richard" forKey:@"name"];
        NSLog(@"%@", [obj valueForKey:@"name"]);
    }
    return 0;
}

つまり、Objective-Cでは、プロパティもしくは決められたルールに従ったsetter/getterを作成すれば、その値を「名前の文字列(ここでは@”name”)」でアクセスすることができます。何も特別なコーディングはいりません。

Key Path

オブジェクトの中にオブジェクトがあり、そのプロパティにKVCを用いてアクセスしたい場合、Key Pathというものを使います。
例として、以下のようにParentクラスがChildクラスのインスタンスを保持していて、さらにChildクラスにnameというプロパティがある場合を考えます。

@interface Child : NSObject
@property(strong) NSString *name;
@end

@implementation Child
@synthesize name;
@end

@interface Parent : NSObject
@property(strong) Child* child;
@end

@implementation Parent
@synthesize child;
@end

この構造において、ParentクラスからいきなりChildクラスのnameプロパティへアクセスする方法を提供するのがKey Pathです。具体的には、以下のようにドットでプロパティ名を区切って指定します。

[parent setValue:@"Jeremy" forKeyPath:@"child.name"];
NSLog(@"%@", [parent valueForKeyPath:@"child.name"]);

1対多関係

先ほど説明したのは、一つの値を持つプロパティです。次は、複数の値を持つプロパティ、つまり1対多の関係を持つ場合です。

NSMutableArray型のプロパティ

当然のことながら、クラスは配列型のプロパティを持つことができ、その配列を先ほど説明したvalueForKeyメソッドで取得することができます。

#import <Foundation/Foundation.h>

@interface MyClass : NSObject
@property(strong) NSMutableArray *languages;
-(id)init;
@end

@implementation MyClass
@synthesize languages;
-(id)init
{
    languages = [NSMutableArray arrayWithObjects: @"Objective-C", @"C++", @"Java", nil];
    return [super init];
}
@end

int main (int argc, const char * argv[])
{

    @autoreleasepool {
        MyClass *obj = [[MyClass alloc]init];
        id array = [obj valueForKey:@"languages"];
        for(int i = 0; i < [array count]; ++i) {
            NSLog(@"%@", [array objectAtIndex:i]);
        }
    }
    return 0;
}

KVCにおける一対多

先ほどの例は、プロパティがNSMutableArray型ですが、そうでない場合はどうすればよいのでしょうか。例えば、独自のデータ構造を用いている場合です。KVCでは、このような場合にも対応できるように、以下の二つのメソッドを実装していれば、KVCにおける一対多の関係と認識できるようにしています。
一対多関係の多側がいくつの要素を持つかを返す:

-(NSUInteger)countOf<Key>

指定したインデックスの要素を返す:

-(id)objectIn<Key>AtIndex:(NSUInteger)index

ちょっとイメージがつきにくいと思いますので、サンプルを以下に示します。わざと、NSMutableArrayを使わず、switch文で特定のインデックスの値を返すようにしています。

#import <Foundation/Foundation.h>

@interface MyClass : NSObject
-(NSUInteger)countOfLanguages;
-(id)objectInLanguagesAtIndex: (NSUInteger)index;
@end

@implementation MyClass
-(NSUInteger)countOfLanguages
{
    return 3;
}
-(id)objectInLanguagesAtIndex: (NSUInteger)index
{
    switch(index) {
        case 0:
            return [NSString stringWithString: @"Objective-C"];
            break;
        case 1:
            return [NSString stringWithString: @"C++"];
            break;
        case 2:
            return [NSString stringWithString: @"Java"];
            break;
    }
    return [NSString stringWithString: @"Error"];
}
@end

int main (int argc, const char * argv[])
{
    @autoreleasepool {
        MyClass *obj = [[MyClass alloc]init];
        id array = [obj mutableArrayValueForKey: @"languages"];
        for(int i = 0; i < [obj countOfLanguages]; ++i) {
            NSLog(@"%@", [array objectAtIndex:i]);
        }
    }
    return 0;
}

ここではlanguagesというプロパティに対して、mutableArrayValueForKeyを用いてアクセスしています。mutableArrayValueForKeyはNSMutableArrayと似たような機能を持つクラスを返します。ですが、実際にはNSMutableArrayではなく、特別なProxyオブジェクトが自動的に生成されて返されます。このProxyは、MyClassのcountOfやobjectInAtIndexにアクセスして、NSMutableArrayのように見せています。