Key Value Coding and Key Value Observing

Key Value Coding1

简介

KVC(Key-value coding)是一套利用字符串标识符间接访问对象属性和关系的机制,由NSKeyValueCoding informal protocal定义。

事实上,KVO定义了NSObject中所需要实现的accessor的符号访问方式和方法名(也就是Dot syntex访问方式和-set<key>-is<key>等obj accessor方法名命名方式)。在Cocoa框架中,Core DataApplication Scriptability2, 绑定(Cocoa Binding)技术和KVO语言特性等,都是以KVC为基础的。

PS: Application Scriptability 和 绑定(Cocoa Binding)是Mac OS X上特有的)。其中Application Scriptability借助KVC来访问和设置model obj[下图]中的数据:

model obj sample

Cocoa框架中,访问obj实例里面的变量需要用到class中定义的accessor方法,包括getter和setter

你也可以用KVC来简化你的代码。

下面两例子使用OS X中 NSTableView / NSOutLineView 绑定行列数据方法的具体实现作对比:

Listing 1 Implementation of data-source method without key-value coding

- (id)tableView:(NSTableView *)tableview
      objectValueForTableColumn:(id)column row:(NSInteger)row {

    ChildObject *child = [childrenArray objectAtIndex:row];
    if ([[column identifier] isEqualToString:@"name"]) {
        return [child name];
    }
    if ([[column identifier] isEqualToString:@"age"]) {
        return [child age];
    }
    if ([[column identifier] isEqualToString:@"favoriteColor"]) {
        return [child favoriteColor];
    }
    // And so on.
}

Listing 2 Implementation of data-source method with key-value coding

- (id)tableView:(NSTableView *)tableview
      objectValueForTableColumn:(id)column row:(NSInteger)row {

    ChildObject *child = [childrenArray objectAtIndex:row];
    return [child valueForKey:[column identifier]];
}

一些基础知识

Model(M) 3

IOS应用开发是遵循MVC设计模式的,Cocoa框架用Object Modeling的规则来规范一个Model的实现。

Object Modeling有如下几个概念的规定:

  • Entity:表示持有数据的一个实体

  • Property:实体中的成员,分为Attribute和:Relationship

  • Attribute:基本类型的成员,比如:数字、NSString。

  • Relationship:指向其它Entity的关系型成员,它又有to 1Relationship和to manyRelationship的区别。

attributes, to-one relationships, and to-many relationships

  • ####attributes
    不可变的简单变量,如字符串、各种标量、Cocoa框架中的NSValue
int counter = 0;
NSTimeInterval interval = 24*60*60;
  • ####to-one relationships
    obj与其property的一对一关系。property,也就是写在interface中得@property,如下方的name、author和publicDate。比起上面的attributes,这些变量所保存的值可以在与其所属obj不变的情况下改变。⬅️这句听起来好别扭……
@interface QCTBook : NSObject

@property NSString *name;
@property NSString *author;
@property NSDate *publicDate;

@end
  • ####to-many relationships
    一对多关系。各种Collection类型,如有序的NSArrayNSMutableArray,无序的NSSetNSMutableSetNSDictionaryNSMutableDictionary
@interface QCTBookself : NSObject

@property NSMutableArray *bookList;

@end
//main

QCTBook *qctBook = [QCTBook new];
QCTBOOkself *qctBookself = [QCTBookself new];

qctBookself.boolList.add(qctBook);

Keys and Key Paths

  • Key

    Key 是用来标识指定property的字符串。一般来说,Key对应到obj的一个accessor方法名或者实例变量名。Key遵循ACSII编码,小写字母开头且不能包含空格。如上面例子中QCTBook obj中的name、author。

  • Key Path

    Key Path是由Key个逗号分隔符组成用来标识obj中property得路径。如上面例子中QCTBookself obj中的bookList.name、bookList.author。

Value

  • Get

    valueForKey:、valueForKeyPath:、dictionaryWithValuesForKeys:

  • Set

    setValue:forKey:、setValue:forKeyPath:、setValuesForKeysWithDictionary:、mutableArrayValueForKey:、mutableSetValueForKey:.

Dot Syntex and Key-Value Coding

例子:

You can use key-value coding methods to access a property, for example, given a class defined as follows:

@interface MyClass
@property NSString *stringProperty;
@property NSInteger integerProperty;
@property MyClass *linkedInstance;
@end

you could access the properties in an instance using KVC:

MyClass *myInstance = [[MyClass alloc] init];
NSString *string = [myInstance valueForKey:@"stringProperty"];
[myInstance setValue:@2 forKey:@"integerProperty"];

这是对比:

To illustrate the difference between the properties dot syntax and KVC key paths, consider the following.

MyClass *anotherInstance = [[MyClass alloc] init];
myInstance.linkedInstance = anotherInstance;
myInstance.linkedInstance.integerProperty = 2;

This has the same result as:

MyClass *anotherInstance = [[MyClass alloc] init];
myInstance.linkedInstance = anotherInstance;
[myInstance setValue:@2 forKeyPath:@"linkedInstance.integerProperty"];

Key-Value Coding Accessor Methods

为了让valueForKey:setValue:forKey:mutableArrayValueForKey:mutableSetValueForKey:等方法生效,需要实现KVC accessor 方法:

本章的accessor pattern是以这样的形式出现:-set<Key>:-<key>。其中key是具体property名。还要注意的区别在于首字母是否大写。

Commonly Used Accessor Patterns

返回property:-<key>。具体可以是object、scalar、data structure。而-is<Key>返回BOOL。

栗子:

使用-<key>:

- (BOOL)hidden{
    //Implementation specific code.
    return ...;
}

使用-is<Key>:

- (BOOL) isHidden {
    //Implementation specific code.
    return ...;
}

为了实现setValue:forKey: ,还需要有下面方法:

- (void)setHidden:(BOOL)flag{
    //Implementation specific code.
    return ...;
}

对于不是object的情况,需要额外地定义setNilValueForKey:方法设置的值:

- (void)setNilValueForKey:(NSString *)theKey {
    if ([theKey isEqualToString:@"hidden"]) {
        [self setValue:@YES forKey:@"hidden"];
} else {
        [super setNilValueForKey:theKey];
    }
}

Collection Accessor Patterns for To-Many Properties

虽然你可以使用-<key>-set<Key>:来设置to-many relationship,但是你应该只在创建collection object的时候才使用他们。为了管理to-many relationship,最好实现针对他们的accessor methods。

好处:

  • 处理to-many relationship的时候性能提升大;
  • 假装成NSArray、NSSet
  • 通过KVO来实现对to-meny relationship property的直接修改

两种collection accessor,分为order和unordered。

Indexed Accessor Pattern

包含方法:

  • counting
  • retrieving
  • adding
  • replacing

实现这些方法之后可以让property用起来像NSArray一样。

如果是NSArray、NSSet直接返回值不写也可以。

返回mutable array方法:mutableArrayValueForKey:

Getter Indexed Accessors

getter方法如下:

  • -countOf<Key> 必要

  • -objectIn<Key>AtIndex:-<key>AtIndexs:其中一个。 必要,对应到NSArray的objectAtIndex:objectsAtIndexes

  • -get<Key>:range: 可选,对应NSArray的getObjects:range:。可以提升性能。

一些具体的栗子:

- (NSUInteger)countOfEmployees {
    return [self.employees count];
}
- (id)objectInEmployeesAtIndex:(NSUInteger)index {
    return [employees objectAtIndex:index];
}

- (NSArray *)employeesAtIndexes:(NSIndexSet *)indexes {
    return [self.employees objectsAtIndexes:indexes];
}
- (void)getEmployees:(Employee * __unsafe_unretained *)buffer range:(NSRange)inRange {
    // Return the objects in the specified range in the provided buffer.

    // For example, if the employees were stored in an underlying NSArray

    [self.employees getObjects:buffer range:inRange];
}
Mutable Indexed Accessors

可以直接实现:
mutableArrayValueForKey:

或者实现这些方法:

  • -insertObject:in<Key>AtIndex:-insert<Key>:atIndexes:其中一个。对应NSArray的insertOjbect:atIndex:insertObjects:atIndexes:

  • -removeObjectFrom<Key>AtIndex:-remove<Key>AtIndexes:其中一个。对应NSArray中的removeObjectAtIndex:removeObjectsAtIndexes:

  • -replaceObjectIn<Key>AtIndex:withObject:replace<Key>AtIndexes:with<Key> 可选。可以提升性能。

具体栗子:

- (void)insertObject:(Employee *)employee inEmployeesAtIndex:(NSUInteger)index {
    [self.employees insertObject:employee atIndex:index];

    return;
}


- (void)insertEmployees:(NSArray *)employeeArray atIndexes:(NSIndexSet *)indexes{
    [self.employees insertObjects:employeeArray atIndexes:indexes];

    return; 
}
- (void)removeObjectFromEmployeesAtIndex:(NSUInteger)index {
    [self.employees removeObjectAtIndex:index];
}


- (void)removeEmployeesAtIndexes:(NSIndexSet *)indexes {
    [self.employees removeObjectsAtIndexes:indexes];
}
- (void)replaceObjectInEmployeesAtIndex:(NSUInteger)index withObject:(id)anObject {
    [self.employees replaceObjectAtIndex:index withObject:anObject];
}


- (void)replaceEmployeesAtIndexes:(NSIndexSet *)indexes withEmployees:(NSArray *)employeeArray {
    [self.employees replaceObjectsAtIndexes:indexes withObjects:employeeArray];
}

Undered Accessor Pattern

NSSet、NSMutableSet

Getter Unordered Accessors

和上面的差不多,只是无序:

  • -countOf<Key> 必要。对应NSSet的count方法

  • -enumeratorOf<Key> 必要。对应NSSet的objectEnumerator

  • menberOf<Key> 必要。对应NSSet的member

栗子:

- (NSUInteger)countOfTransactions {
    return [self.transactions count];
}

- (NSEnumerator *)enumeratorOfTransactions {
    return [self.transactions objectEnumerator];
}

- (Transaction *)memberOfTransactions:(Transaction *)anObject {
    return [self.transactions member:anObject];
}


Mutable Unordered Accessors

实现mutableSetValueForKey:直接返回

或者:

  • -add<Key>Object:-add<Key>:其中一个。必要。对应NSMutableSet中的addObject:

  • -remove<Key>Object:-remove<Key>:其中一个。必要。对应NSMutableSet的removeObject:

  • -intersect<Key>:可选。对应NSSet中intersectSet:

栗子:

- (void)addTransactionsObject:(Transaction *)anObject {
    [self.transactions addObject:anObject];
}

- (void)addTransactions:(NSSet *)manyObjects {
    [self.transactions unionSet:manyObjects];
}
- (void)removeTransactionsObject:(Transaction *)anObject {
      [self.transactions removeObject:anObject];
}

- (void)removeTransactions:(NSSet *)manyObjects {
      [self.transactions minusSet:manyObjects];
}
- (void)intersectTransactions:(NSSet *)otherObjects {
    return [self.transactions intersectSet:otherObjects];
}

Key-Value Validation

KVO 提供用于做验证的API

Validation Method Naming Convention

格式:validate<Key>:error:

栗子:一个传入参数和一个错误传出地址,返回验证结果BOOL

- (BOOL) validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError {
    //Implementation specific code.
    return ...;
}

Implementing a Validation Method

三种判断结果:

  1. 验证通过返回YES,NSError为空。
  2. 传入值验证不通过返回NO,NSError描述验证失败原因。
  3. 创建一个新值并返回YES,NSError为空。不鼓励这么做。

栗子:

-(BOOL)validateName:(id *) error:(NSError * __autoreleaseing *)outError{

    //The name must not be nil, and must be at least two characters long.
    if ((*ioValue == nil) || ([(NSString *)ioValue length] < 2)){
        if (outError != NULL){
            NSString *errorString = NSLocaloizedString(@"A Person's name must be at least two characters long",@"validation:Person, too short name error");
            NSDictionary *userInfoDict = @{NSLocalizedDescriptionKey:errorString};
            *outError = [[NSError alloc] initWithDomain:PERSON_ERROR_DOMAIN code:PERSON_INVALID_NAME_CODE userInfo:userInfoDict];
        }
        return NO;
    }
    return YES;
}

注意要检查传入的NSError是否非空。

Invoking Validation Methods

调用方法:validateValue:forKey:error:,该函数会根据key搜索对应的validate<Key>:eoor:方法并调用。如果对应方法不存在的话直接返回YES。

不能在-set<Key>:里面调用验证方法。

Automatic Validation

使用Cocoa Binding 可以实现自动调用验证方法,但是只有OS X才能用。

Validation of Scalar Values

non-object自动封装成NSValue、NSNumber。

- (BOOL) validateAge:(id *)ioValue error:(NSError * _autoreleasing *)outError{

    if (ioValue == nil){
        //Trap this in setNilValueForKey.
        //an alertnative might be to create new NSNumber with value 0 here.
        return YES;
    }

    if ([*ioValue floatValue] <= 0.0){
        if (outError != NULL){
            NSString *errorString = NSLocalizedFromTable(@"",@"");
            NSDictionary *userInfoDict = @{NSLocalizedDescriptionKey: errorString};
            NSError *error = [[NSError alloc] initWithDomian:PERSON_ERROR_DOMIAN code:PERSON_INVALID_AGE_CODE userInfo:userInfoDict]];
        }
    }else{
        return YES'
    }
    // ...
}

Ensuring KVC Compliance

保证一个类的KVO约束正常所需要完成的步骤:

Attribute and To-One Relationship Compliance

  • 实现-<key>-is<Key>或者有实例变量 <key>_<key>。对于URL,KVC允许key大写。
  • 如果property是可修改的,实现-set<Ket>:
  • -set<Key>实现不能包含验证步骤。
  • 如果需要验证的话实现validate<Key>:error:

Indexed To-Many Relationship Compliance

  • 实现可以返回整个数值的-<key>
  • 或者有直接有一个实例变量<key>_<key>
  • 或者实现-countOf<Key>-objectIn<Key>AtIndex:-<key>AtIndexes:
  • 可选。实现get<Key>:range:来提升性能。

mutable的话还要:

  • 实现-insertObject"in<Key>AtIndex:" 或者 -insert<Key>:atIndexes:
  • 实现-removeObjectFrom<Key>AtIndexes: 或者 -remove<Key>AtIndexes:
  • 可选。实现replaceObjectIn<Key>AtIndex:withObject: 或者 -replace<Key>AtIndexes:with<Key>:来提升性能。

Unordered To-Many Relationship Compliance

  • 实现方法-<key>来返回set。
  • 或者设置一个实例变量<key> 或者 _<key>
  • 或者实现-countOf<Key>-enumeratorOf<key>-memberOf<Key>

mutable情况:

  • 实现至少一个:-add<Key>Ojbect:-add<Key>:
  • 实现至少一个:-remove<Key>Object:-remove<Key>:
  • 可选。实现-intersect<Key>:-set<Key>: 提升性能。

Scalar and Structure Support

KVC 提供对non-object类型数据的自动封装(NSValue、NSNumber)

Represent Non-Object Values

valueForKey: 如果结果值不是一个object,将创建NSValue、NSNumber来封装后返回。

setValue:forKey: 如果赋值对象不是object会自动调用传入object的-<type>Value来转换或再赋值。

Handing nil Values

由于不能对non-object赋值nil,需要重写-setNilValueForKey:方法来阻止默认抛出NSInvalidArgumentException:

- (void)setNilValueForKey:(NSString *)theKey{
    if ([theKey isEqualToString:@"age"]){
        [self setValue[NSNumber numberWithFloat:0.0] forKey:@"age"];
    }else{
        [super setNilValueForKey:theKey];
    }
}

对于一些二进制数据的写入可能会需要重写-unableToSetNilForKey:

Wrapping and Unwrapping Scaler Types

标量类型,使用NSNumber封装。

记一下:

Data type Creationg method Accessor method
BOOL numberWithBool: boolValue
char numberWithChar: charValue
double numberWithDouble: doubleValue
float numberWithFloat: floatValue
int numberWithInt: intValue
long numberWithLong: longValue
long long numberWithLongLong: longLongValue
short numberWithShort: shortValue
unsigned char numberWithUnsingedCahr: unsignedChar
unsigned int numberWithUnsignedInt: unsignedInt
unsigned long numberWithUnsignedLong: unsignedLong
unsigned long long numberWirhUsignedLongLong: unsignedLongLong
unsigned short numberWithUsgnedShort: unsgnedShort

Wrapping and Unwrapping Structs

结构体类型,使用NSValue封装。

Data type Creation method Accessor method
NSPoint valueWithPoint: pointValue
NSRange valueWithRange: rangeValue
NSRect valueWithRect:(OS X only) rectValue
NSSize valueWithSize: sizeValue

栗子:


typedef struct { float x,y,z; } ThreeFloats @interface MyClass //sending an instance of MyClass the message valueForKey: with the parameter @"threeFloats" will invoke the MyClass method threeFloats and return the result wrapped in an NSValue. - (void)setThreeFloats:(ThreeFloats)theFloats; //Sending the instance of MyClass a setValue:forKey: message with an NSValue object wrapping a ThreeFloats struct will invoke setThreeFloats: and pass the result of sending the NSValue object getValue: message. - (ThreeFloats)threeFloats; @end

该机制并不负责MRR……

标签:ios, object-c, kvc, kvo