欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

KVC详解

程序员文章站 2024-03-24 14:04:16
...

关于KVC

KVC为遵守NSKeyValueCoding协议的对象提供间接的方式来访问它们的属性。当对象符合KVC,属性能通过字符串来进行访问,也可以通过实例变量和对应的访问方法访问属性。

获取访问方法返回属性的值,设置访问方法设置属性的值。在OC,你可以使用实例变量访问属性。虽然这些方式可以直接访问属性,但是需要使用特定的访问方法和实例变量。相反,KVC对象提供简单的方法来统一访问所有属性。

KVC是构成其他cocoa技术的基本概念,例如,KVO,cocoa-binding,Core Data,AppleScript-ability。KVC也会简化你的代码。

使用KVC对象

当对象继承NSObject,它就实现了KVC。你可以实现下面的任务:

• 访问对象属性。

• 操作集合属性。

• 使用集合操作符。

• 访问非对象属性。

• 通过键路径访问属性。

为对象符合KVC要求

继承NSObject的对象默认实现KVC。为了让KVC更好的实现,你需要确保访问方法和实例变量的声明符合命名规则。你也可以扩展和自定义KVC方法。


访问对象属性

对象的属性(property)可以声明在类的接口声明和种类的接口声明。

• 属性(attribute)。它们是简单的值,例如,数值,字符串,布尔值。值对象NSNumber。

• 一对一关系。对象属性。

• 一对多关系。集合属性。

@interface BankAccount : NSObject
 
@property (nonatomic) NSNumber* currentBalance;              // An attribute
@property (nonatomic) Person* owner;                         // A to-one relation
@property (nonatomic) NSArray< Transaction* >* transactions; // A to-many relation
 
@end

使用键和键路径来标记对象属性

键一般使用属性的名称,但不能有空格,通常小写开头。键路径使用点划分的多键字符串来表达要访问对象属性的顺序路径。第一个键是对应访问对象属性的值,接下来的键对应访问这个属性值的属性值。

使用键获取属性的值

• valueForKey: - 返回键名对应的值。如果提供的键名不能通过访问搜索模式获取值,对象会调用valueForUndefinedKey:方法。这个方法默认会抛出一个NSUndefinedKeyException异常,你可以在子类重写这个方法来处理这种情况。

• valueForKeyPath: - 返回键路径对应的值。如果在键路径序列中,某个键不是对应一个KVC对象,那么valueForKey: 不能找到访问方法,之后对象调用valueForUndefinedKey:方法。

• dictionaryWithValuesForKeys: - 返回一组键对应的NSDictionary对象,这个方法会依次调用valueForKey:方法来获取对应键的值。这个字典包含所有的键和值。

注意:由于不可变的集合是不能存储nil值,所以KVC会自动地把nil值转换为NSNull对象。

当你使用键路径来标记属性,如果最后的键对应访问一对多关系(对象集合)的属性值,返回的值是一个包含对象集合的每个对象属性值的集合。

使用键设置属性的值

• setValue:forKey: - 设置键名对应属性的值。默认会自动解包装NSNumber和NSValue为指定的值类型。如果指定的键名没有找到设置方法,对象会调用setValue:forUndefinedKey:方法。这个方法默认会抛出一个NSUndefinedKeyException异常,你可以在子类重写这个方法来处理这种情况。

• setValue:forKeyPath: - 设置键路径对应属性的值。如果在键路径序列中,某个键不是对应一个KVC对象,那么对象会对象调用valueForUndefinedKey:方法。

• setValuesForKeysWithDictionary: - 使用NSDictionary设置多个键对应属性的值。默认会依次调用setValue:forKey:方法,对NSNULL对象会解析成nil。

在系统默认实现中,设置不是对象属性的值为nil时,对象会调用setNilValueForKey:方法。这个方法默认会抛出一个NSInvalidArgumentException异常,你可以在子类重写这个方法来处理这种情况。

使用键来简化对象访问

不用KVC:

- (id)tableView:(NSTableView *)tableview objectValueForTableColumn:(id)column row:(NSInteger)row
{
    id result = nil;
    Person *person = [self.people objectAtIndex:row];
 
    if ([[column identifier] isEqualToString:@"name"]) {
        result = [person name];
    } else if ([[column identifier] isEqualToString:@"age"]) {
        result = @([person age]);  // Wrap age, a scalar, as an NSNumber
    } else if ([[column identifier] isEqualToString:@"favoriteColor"]) {
        result = [person favoriteColor];
    } // And so on...
 
    return result;
}

使用KVC:

- (id)tableView:(NSTableView *)tableview objectValueForTableColumn:(id)column row:(NSInteger)row
{
    return [[self.people objectAtIndex:row] valueForKey:[column identifier]];
}


访问集合属性

一对多关系属性和其他属性一样,可以使用其他属性的KVC访问方法(valueForKey: ,setValue:forKey:)。当你操作集合内容时,使用KVC可变集合代理方法会更有效。

• mutableArrayValueForKey: 和mutableArrayValueForKeyPath: - 返回一个NSMutableArray代理对象(不是NSMutableArray,但可以像NSMutableArray一样操作)。

• mutableSetValueForKey: 和mutableSetValueForKeyPath: - 返回一个NSMutableSet代理对象(不是NSMutableSet,但可以像NSMutableSet一样操作)。

• mutableOrderedSetValueForKey: 和mutableOrderedSetValueForKeyPath: - 返回一个NSMutableOrderedSet代理对象(不是NSMutableOrderedSet,但可以像NSMutableOrderedSet一样操作)。

当你操作代理对象,添加对象,删除对象或者替换对象,代理对象默认会修改对应的属性值。这可能比valueForKey:返回的不可变集合更有效,valueForKey:这种方式会再创建一个已修改的集合并重新调用setValue:forKey:方法来设置属性值。这可能比直接访问可变集合属性更有效。这些KVC可变集合代理方法会提供额外的好处去进行KVO的集合属性操作。

使用集合操作符

当你对KVC对象使用valueForKeyPath:方法时,你可以嵌入集合操作符在键路径。集合操作符是一个带有@前缀的关键字,它会在返回数据前进行一些操作。

当键路径包含集合操作符,操作符左边的键路径为左键路径,指示要对左键路径的属性值进行操作。如果你直接对集合对象使用集合操作符,例如NSArray实例,左操作符可以省略。

KVC详解

集合操作符有3种基本类型:

• 聚合操作符(Aggregation Operators)合并集合对象并返回一个单独对象。这个对象会匹配右键路径的属性的数据类型。@count操作符除外,它没有右键路径而且总是返回NSNumber实例。

• 数组操作符(Array Operators)返回NSArray实例,它包含对应集合的子集对象。

• 嵌套操作符(Nesting Operators)操作嵌套集合(被集合包含的集合)并返回NSArray或者NSSet实例。返回的集合包含对象的属性,这个对象被嵌套集合包含。

简单数据

下面将会使用这些数据进行说明。

@interface BankAccount : NSObject
 
@property (nonatomic) NSNumber* currentBalance;              // An attribute
@property (nonatomic) Person* owner;                         // A to-one relation
@property (nonatomic) NSArray< Transaction* >* transactions; // A to-many relation
 
@end
@interface Transaction : NSObject
 
@property (nonatomic) NSString* payee;   // To whom
@property (nonatomic) NSNumber* amount;  // How much
@property (nonatomic) NSDate* date;      // When
 
@end
聚合操作符

聚合操作符操作NSArray或者NSSet集合属性,返回一个单独的值来反映聚合结果。

@avg

当你使用@avg操作符,valueForKeyPath:会依次读取每一个右键路径的属性值,转化它们为double值(nil值为0)并计算平均值,最后返回平均值的NSNumber实例。

NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];

@count

当你使用@count操作符,valueForKeyPath:返回集合的对象数量,右操作符忽略。

NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@"@count"];

@max

当你使用@max操作符,valueForKeyPath:会搜索右键路径的属性值的最大值。搜索使用compare:方法进行比较,大多数Foundation对象都有这个方法,例如NSNumber对象。右键路径对应的对象必须有意义地响应这个方法。搜索会避免nil值。

NSDate *latestDate = [self.transactions valueForKeyPath:@"@max.date"];

@min

当你使用@min操作符,valueForKeyPath:会搜索右键路径的属性值的最小值。搜索使用compare:方法进行比较,大多数Foundation对象都有这个方法,例如NSNumber对象。右键路径对应的对象必须有意义地响应这个方法。搜索会避免nil值。

NSDate *earliestDate = [self.transactions valueForKeyPath:@"@min.date"];

@sum

当你使用@sum操作符,valueForKeyPath:会依次读取每一个右键路径的属性值,转化它们为double值(nil值为0)并计算总和值,最后返回平均值的NSNumber实例。

NSNumber *amountSum = [self.transactions valueForKeyPath:@"@sum.amount"];
数组操作符

数组操作符会让valueForKeyPath: 返回对应右键路径对应属性的数组集合的特殊子集合。

注意:如果使用数组操作符遇到叶子对象为nil,valueForKeyPath:会抛出异常。

@distinctUnionOfObjects

当你使用@distinctUnionOfObjects操作符,valueForKeyPath:返回右键路径对应属性的一个包含不同对象的集合。

NSArray *distinctPayees = [self.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];

@unionOfObjects

当你使用@unionOfObjects操作符,valueForKeyPath:返回右键路径对应属性的一个包含所有对象的集合,它不会删除重复对象。

NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];
嵌套操作符

嵌套操作符操作嵌套集合,这个集合被集合包含。

注意:如果使用嵌套操作符遇到叶子对象为nil,valueForKeyPath: 会抛出异常。

嵌套集合的例子:

NSArray* moreTransactions = @[<# transaction data #>];
NSArray* arrayOfArrays = @[self.transactions, moreTransactions];

@distinctUnionOfArrays

当你使用@distinctUnionOfArrays操作符,valueForKeyPath:返回右键路径对应属性的一个包含不同对象的集合(操作嵌套集合)。

NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];

@unionOfArrays

当你使用@unionOfArrays操作符,valueForKeyPath:返回右键路径对应属性的一个包含所有对象的集合(操作嵌套集合),它不会删除重复对象。

NSArray *collectedPayees = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.payee"];

@distinctUnionOfSets

当你使用@distinctUnionOfSets操作符,valueForKeyPath:返回右键路径对应属性的一个包含不同对象的集合(操作嵌套集合)。和@distinctUnionOfArrays不同的是嵌套集合是NSSet被NSSet包含而不是NSArray被NSArray包含,同样它返回的是NSSet实例而不是NSArray实例。


代表非对象值

KVC对象会处理对象属性和非对象属性。默认会自动对对象参数或者返回值和非对象属性之间进行转换。这允许获取方法和设置方法存储的属性为结构体和数值。

当你执行KVC协议中的获取方法时,例如valueForKey:,默认实现会根据访问搜索模式和键来确定访问方法和实例变量。如果返回值不是对象,获取方法会使用返回值创建NSNumber(数值)或者NSValue(结构体)对象来代替返回值。

同样的,setValue:forKey:方法会根据访问方法或者实例变量和指定的键来决定数据类型。如果数据类型不是对象,设置方法首先会向设置的Value对象发送<type>Value方法来获取底层数据并使用它来设置值。

注意:如果向KVC的设置方法提供nil值,会触发setNilValueForKey:方法。默认这个方法会抛出NSInvalidArgumentException异常。你可以在子类重写这个方法来处理这种情况。

包装和解包装数值类型

下面这些类型会使用NSNumber来包装和解包装。

KVC详解

包装和解包装结构体

下面这些类型会使用NSValue来包装和解包装。

KVC详解

如果不是NSPoint、NSRange、NSRect,NSSize的结构体,KVC也可以自动地使用NSValue来包装和解包装它(它们的类型编码字符串必须以“{”开头)。

typedef struct {
    float x, y, z;
} ThreeFloats;
 
@interface MyClass
@property (nonatomic) ThreeFloats threeFloats;
@end
NSValue* result = [myClass valueForKey:@"threeFloats"];
ThreeFloats floats = {1., 2., 3.};
NSValue* value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[myClass setValue:value forKey:@"threeFloats"];


验证属性

KVC支持属性验证。就像你使用基于键来读取和设置KVC对象属性,你可以通过键或者键路径验证属性。当你调用validateValue:forKey:error:或者validateValue:forKeyPath:error:方法,默认会搜索对象是否接受验证方法(或者键路径最后一个键),这个方法的命名符合validate<Key>:error:。如果对象没有定义这个方法,默认验证成功并返回YES。当特定属性验证方法存在,返回值由具体方法实现。

因为指定属性方法接收值对象和错误对象的引用,验证有3种可能的输出:

1. 认为值对象是有效的并返回YES而且没有修改值对象或者错误对象。

2. 认为值对象是无效的并且没有修改它。在这种情况下,返回NO并设置错误对象为NSError对象实例。

3. 认为值对象是无效的,但是创建新的有效值对象来代替它。在这种情况下,返回YES并且不改动错误对象。这个方法修改值对象引用去指向新的对象。你应该总是创建新的对象而不是修改原来的值对象,即使它是可变的。

Person* person = [[Person alloc] init];
NSError* error;
NSString* name = @"John";
if (![person validateValue:&name forKey:@"name" error:&error]) {
    NSLog(@"%@",error);
}
自动验证

通常,KVC协议方法和KVC的默认实现都没有执行自动验证。你要适当地在APP应用这些验证方法。

其他的cocoa技术可能会执行自动验证。例如Core Data会在上下文保存时自动验证属性。


访问搜索模式

默认实现KVC协议的NSObject对象会使用一定的规则映射键来访问属性。这些协议方法使用键参数来搜索实例的访问方法和实例变量以及相关符合一些命名规则的方法。虽然你可以修改默认的搜索,了解默认搜索是怎样执行的会对你有帮助。例如跟踪KVC对象的行为和更好地定义符合KVC的类。

注意:下面的说明会使用<key>作为键的占位符。

基本获取方法的搜索模式

默认valueForKey:的实现使用key来获取属性名为key的值。

1. 按顺序寻找对象的get<Key>,<key>,is<Key>,或者_<key>访问方法,如果发现,进行步骤5,否则进行下一步。

2. 如果没有发现简单访问方法,寻找countOf<Key>和objectIn<Key>AtIndex:(对应NSArray的原始方法)或者<key>AtIndexes:方法 (对应NSArray的objectsAtIndexes:)。

如果发现第一个方法和后面两个方法的其中一个,那么会创建一个集合代理对象,这个对象会响应所有NSArray声明的方法。否则进行步骤3。

这个代理对象会进行一系列NSArray方法的转换,这些方法对应调用countOf<Key>和objectIn<Key>AtIndex:或者<key>AtIndexes:的其中一个方法的组合。如果原始对象实现可选方法get<Key>:range:,代理对象也会在适当时使用这个方法。实际上,代理对象会和KVC对象互相作用,就像KVC对象拥有一个NSArray属性(对应代理对象),虽然对象没有这个属性。

3. 如果没有发现简单访问方法和一组数组访问方法,寻找countOf<Key>,enumeratorOf<Key>,memberOf<Key>: 方法(对应NSSet的原始方法)。

如果这3个方法都发现,那么会创建一个集合代理对象,这个对象会响应所有NSSet声明的方法。否则进行步骤4。

这个代理对象会进行一系列NSSet方法的转换,这些方法对应调用countOf<Key>,enumeratorOf<Key>,memberOf<Key>: 的组合。实际上,代理对象会和KVC对象互相作用,就像KVC对象拥有一个NSSet属性(对应代理对象),虽然对象没有这个属性。

4. 如果没有发现简单访问方法和一组集合访问方法而且KVC对象的类属性accessInstanceVariablesDirectly为YES,按顺序寻找对象的_<key>,_is<Key>,<key>,或者is<Key>实例变量。如果发现,直接获取实例变量的值并进行步骤5,否则进行步骤6。

5. 如果获取的属性值是对象指针,简单地返回它。

如果属性值是支持NSNumber的数值,创建NSNumber对象并返回它。

如果属性值不支持NSNumber,转换为NSValue对象并返回它。

6. 如果都失败了,调用valueForUndefinedKey:方法。这个方法默认会抛出异常,你可以在子类重写这个方法来处理这种情况。

基本设置方法的搜索模式

默认setValue:forKey:的实现使用key和value来设置属性名为key的值。

1. 按顺序寻找对象的set<Key>: 或者_set<Key>访问方法。如果发现,调用这个方法来设置值(值可能需要解包装)。

2. 如果没有发现简单访问方法而且KVC对象的类属性accessInstanceVariablesDirectly为YES,按顺序搜索对象的_<key>,_is<Key>,<key>,或者is<Key>实例变量。如果发现,直接设置实例变量的值。

3. 如果没有发现简单访问方法和实例变量,调用setValue:forUndefinedKey:方法。这个方法默认会抛出异常,你可以在子类重写这个方法来处理这种情况。

可变数组的搜索模式

默认mutableArrayValueForKey:的实现使用key获取属性名为key的可变代理数组。

1. 寻找insertObject:in<Key>AtIndex:和removeObjectFrom<Key>AtIndex:方法(对应NSMutableArray的insertObject:atIndex:和removeObjectAtIndex:原始方法)或者insert<Key>:atIndexes:和remove<Key>AtIndexes:方法(对应NSMutableArray的insertObjects:atIndexes:和removeObjectsAtIndexes:原始方法)。

如果对象至少有一个插入方法和至少一个删除方法,返回一个集合代理对象,这个对象响应NSMutableArray的所有方法。

这个代理对象会进行一系列NSMutableArray方法的转换,这些方法对应调用insertObject:in<Key>AtIndex:,removeObjectFrom<Key>AtIndex:,insert<Key>:atIndexes:,remove<Key>AtIndexes:的结合。

当KVC对象实现可选方法replaceObjectIn<Key>AtIndex:withObject:或者replace<Key>AtIndexes:with<Key>:方法,代理对象会在适当的时候使用这些方法来获取最好的性能。

2. 如果没有发现可变数组方法,寻找set<Key>:访问方法。在这种情况下,返回代理对象响应NSMutableArray方法,代理对象通过调用set<Key>:方法来响应这些方法。

注意:在第2步中返回的代理对象比第1步返回的代理对象效率要低,因为会重复地创建新的集合对象而不是删除和修改已经创建的集合对象。因此,你应该尽量避免这样做。

3. 如果没有发现可变数组方法和set<Key>:访问方法而且KVC对象的类属性accessInstanceVariablesDirectly为YES,按顺序寻找实例变量_<key>或者<key>。

如果发现这些实例变量,返回代理对象响应NSMutableArray方法,代理对象会转发消息给实例变量,这个实例变量可能是NSMutableArray实例或者子类。

4. 如果都失败,返回代理对象。如果向代理发送NSMutableArray方法,会调用对象的setValue:forUndefinedKey:方法。这个方法默认会抛出异常,你可以在子类重写这个方法来处理这种情况。

可变有序集合的搜索模式

默认mutableOrderedSetValueForKey: 的实现使用key获取属性名为key的可变有序集合。

1. 寻找insertObject:in<Key>AtIndex:和removeObjectFrom<Key>AtIndex:方法(对应NSMutableOrderedSet的原始方法)以及insert<Key>:atIndexes: 和remove<Key>AtIndexes:方法(对应NSMutableOrderedSet的insertObjects:atIndexes:和removeObjectsAtIndexes:方法)。

如果对象至少有一个插入方法和至少一个删除方法,返回一个集合代理对象,这个对象响应NSMutableOrderedSet的所有方法。

这个代理对象会进行一系列NSMutableOrderedSet方法的转换,这些方法对应调用insertObject:in<Key>AtIndex:,removeObjectFrom<Key>AtIndex:,insert<Key>:atIndexes:,remove<Key>AtIndexes:的结合。

当KVC对象实现可选方法replaceObjectIn<Key>AtIndex:withObject:或者replace<Key>AtIndexes:with<Key>:方法,代理对象会在适当的时候使用这些方法来获取最好的性能。

2. 如果没有发现可变数组方法,寻找set<Key>:访问方法。在这种情况下,返回代理对象响应NSMutableOrderedSet方法,代理对象通过调用set<Key>:方法来响应这些方法。

注意:在第2步中返回的代理对象比第1步返回的代理对象效率要低,因为会重复地创建新的集合对象而不是删除和修改已经创建的集合对象。因此,你应该尽量避免这样做。

3. 如果没有发现可变数组方法和set<Key>:访问方法而且KVC对象的类属性accessInstanceVariablesDirectly为YES,按顺序寻找实例变量_<key>或者<key>。

如果发现这些实例变量,返回代理对象响应NSMutableOrderedSet方法,代理对象会转发消息给实例变量,这个实例变量可能是NSMutableOrderedSet实例或者子类。

4. 如果都失败,返回代理对象。如果向代理发送NSMutableOrderedSet方法,会调用对象的setValue:forUndefinedKey:方法。这个方法默认会抛出异常,你可以在子类重写这个方法来处理这种情况。
可变集合的搜索模式

默认mutableSetValueForKey:的实现使用key获取属性名为key的可变集合。

1. 寻找add<Key>Object:和remove<Key>Object:方法(对应NSMutableSet的addObject:和removeObject:原始方法)以及add<Key>:和remove<Key>:方法(对应NSMutableSet的unionSet:和minusSet:方法)。

如果对象至少有一个添加方法和至少一个删除方法,返回一个集合代理对象,这个对象响应NSMutableSet的所有方法。

这个代理对象会进行一系列NSMutableSet方法的转换,这些方法对应调用add<Key>Object:,remove<Key>Object:,add<Key>:,remove<Key>:的结合。

当KVC对象实现可选方法intersect<Key>: 或者set<Key>:方法,代理对象会在适当的时候使用这些方法来获取最好的性能。

2. 如果KVC对象是Core Data管理对象,这个搜索模式不会继续下去。

3. 如果没有发现可变集合方法而且也不是Core Data管理对象,寻找set<Key>:访问方法。在这种情况下,返回代理对象响应NSMutableSet方法,代理对象通过调用set<Key>:方法来响应这些方法。

注意:在第3步中返回的代理对象比第1步返回的代理对象效率要低,因为会重复地创建新的集合对象而不是删除和修改已经创建的集合对象。因此,你应该尽量避免这样做。

4. 如果没有发现可变集合方法和set<Key>:访问方法而且KVC对象的类属性accessInstanceVariablesDirectly为YES,按顺序寻找实例变量_<key>或者<key>。

如果发现这些实例变量,返回代理对象响应NSMutableArray方法,代理对象会转发消息给实例变量,这个实例变量可能是NSMutableSet实例或者子类。

5. 如果都失败,返回代理对象。如果向代理发送NSMutableSet方法,会调用对象的setValue:forUndefinedKey:方法。这个方法默认会抛出异常,你可以在子类重写这个方法来处理这种情况。


符合基本的KVC

当对象实现KVC,你依赖于NSObject的默认实现。默认实现也依赖于你定义的实例变量和访问方法。

你可以使用@property来定义属性,编译器会自动生成实例变量和访问方法而且会符合KVC默认实现的要求。

如果你手动定义实例变量和访问方法,你需要符合KVC默认实现的约定。你可以添加额外方法来提高对象的集合属性的交互和支持属性验证。

基本获取方法

你可以在获取方法添加额外的处理,你可以使用属性名作为方法名。

- (NSString*)title
{
   // Extra getter logic…
 
   return _title;
}

对于布尔值,你需要添加is前缀。

- (BOOL)isHidden
{
   // Extra getter logic…
 
   return _hidden;
}

如果属性值是数值或者结构体,你不需要添加特殊处理,因为KVC默认实现会包装和解包装它。

基本设置方法

你可以在设置方法添加额外的处理,你需要在属性名前添加set前缀作为设置方法名。

- (void)setHidden:(BOOL)hidden
{
    // Extra setter logic…
 
   _hidden = hidden;
}

注意:不要在设置方法调用属性验证方法。

当属性值不是对象属性,如果在KVC设置方法使用nil设置,会触发setNilValueForKey:方法。

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

你可以适当地提供上面的方法,即使你让编译器同步生成设置方法。

实例变量

当KVC默认实现没有发现任何访问方法,它会检查accessInstanceVariablesDirectly的类实例变量是否为YES来直接访问实例变量。默认这个值为YES,你可以在子类重写为NO。

如果你允许访问实例变量,在属性名加下滑线前缀作为实例变量名。通常编译器会自动生成这种形式的实例变量,你可以使用@synthesize指令来使用自己定义的实例变量名。

@synthesize title = _title;
在一些情况下,不是使用@synthesize指令和编译器自动生成属性,而是使用@dynamic指令来告诉编译器自己会在运行时提供获取和设置访问方法。这样就可以避免自动同步获取方法以便你可以提供集合获取方法。在这种情况下,你还要在接口声明中声明实例变量。
@interface MyObject : NSObject {
    NSString* _title;
}
 
@property (nonatomic) NSString* title;
 
@end
定义集合访问方法

当你使用约定的命名来声明实例变量和访问方法,KVC默认实现会定位到它们并响应KVC声明的方法。这同样适用于一对多关系的集合属性。但是,你可以提供集合访问方法来实现一对多关系集合属性。

• 不使用NSArray或者NSSet的一对多关系。当你提供集合访问方法,KVC默认实现会返回一个代理对象,这个代理对象会调用这些方法来响应NSArray或者NSSet方法。内部操作的属性对象可以不是NSArray或者NSSet,因为代理对象会使用你的集合访问方法来提供预期的行为。

• 提高使用一对多关系的可变版本的性能。在响应每次变化时,不是使用设置方法来重复创建新的集合对象,而是使用你的集合访问方法来修改你内部的属性。

• 符合KVO来允许访问你的集合属性内容。

根据你需要实现索引、有序的集合(NSArray)还是无序、唯一的集合(NSSet),你可以实现这2组集合访问方法的其中一组。在其他情况,你至少有一套获取访问方法来读取属性值和添加额外一组方法**可变集合内容。

访问有序集合

你添加有序访问方法来支持计数,获取,添加,替换有序集合对象。内部对象一般是NSArray或者NSMutableArray,但你可以提供集合访问方法来**任何属性,这个属性会看起来像数组。

有序集合的获取方法

集合属性默认没有获取方法,如果你提供有序集合的获取方法,KVC默认实现会在valueForKey:返回数组代理对象,它会调用集合访问方法来完成工作。

注意:编译器默认会同步每一个属性,所以默认实现不会返回代理对象。你可以不声明属性(完全依赖实例变量)或者使用@dynamic声明属性,来告诉编译器在运行时提供访问方法。这样编译器不会提供获取方法而且会使用下面的集合访问方法。

• countOf<Key>

这个方法返回一对多关系对象的数量,就像NSArray的原始方法count。当你的内部属性为NSArray,你可以直接使用这个方法。

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

• objectIn<Key>AtIndex:或者<key>AtIndexes

- (id)objectInTransactionsAtIndex:(NSUInteger)index {
    return [self.transactions objectAtIndex:index];
}
 
- (NSArray *)transactionsAtIndexes:(NSIndexSet *)indexes {
    return [self.transactions objectsAtIndexes:indexes];
}

• get<Key>:range:

- (void)getTransactions:(Transaction * __unsafe_unretained *)buffer
               range:(NSRange)inRange {
    [self.transactions getObjects:buffer range:inRange];
}
可变有序集合

支持一对多可变关系的有序访问需要实现一组不同的访问方法。当你提供这些设置方法,默认实现会响应mutableArrayValueForKey:方法并返回NSMutableArray代理对象,这个代理对象会使用你的集合访问方法。这比直接返回NSMutableArray对象更有效。这个一对多可变关系也符合KVO。

为了实现可变有序的一对多关系,实现下面额外方法:

• insertObject:in<Key>AtIndex:或者insert<Key>:atIndexes:

- (void)insertObject:(Transaction *)transaction
  inTransactionsAtIndex:(NSUInteger)index {
    [self.transactions insertObject:transaction atIndex:index];
}
 
- (void)insertTransactions:(NSArray *)transactionArray
              atIndexes:(NSIndexSet *)indexes {
    [self.transactions insertObjects:transactionArray atIndexes:indexes];
}

• removeObjectFrom<Key>AtIndex:或者remove<Key>AtIndexes:

- (void)removeObjectFromTransactionsAtIndex:(NSUInteger)index {
    [self.transactions removeObjectAtIndex:index];
}
 
- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
    [self.transactions removeObjectsAtIndexes:indexes];
}

• replaceObjectIn<Key>AtIndex:withObject:或者replace<Key>AtIndexes:with<Key>:

可选地提供这些方法可以提高性能。

- (void)replaceObjectInTransactionsAtIndex:(NSUInteger)index
                             withObject:(id)anObject {
    [self.transactions replaceObjectAtIndex:index
                              withObject:anObject];
}
 
- (void)replaceTransactionsAtIndexes:(NSIndexSet *)indexes
                    withTransactions:(NSArray *)transactionArray {
    [self.transactions replaceObjectsAtIndexes:indexes
                                withObjects:transactionArray];
}
获取无序集合

你添加无序集合访问方法来提供访问可变无序集合。通常,这个关系是NSSet或者NSMutableSet实例。但是,当你实现这些访问方法,你可以使用任何类(包括NSSet、NSMutableSet)来实现这种关系,KVC默认实现返回的代理对象的行为像NSSet一样。

无序集合获取方法

当你提供下面的集合获取方法去返回集合的对象数量,遍历集合对象,检查集合是否存在对象。KVC默认实现会响应valueForKey:方法并返回NSSet代理对象,这个代理对象使用这些获取方法来进行工作。

注意:编译器默认会同步每一个属性,所以默认实现不会返回代理对象。你可以不声明属性(完全依赖实例变量)或者使用@dynamic声明属性,来告诉编译器在运行时提供访问方法。这样编译器不会提供获取方法而且会使用下面的集合访问方法。

• countOf<Key>

- (NSUInteger)countOfEmployees {
    return [self.employees count];
}

• enumeratorOf<Key>

- (NSEnumerator *)enumeratorOfEmployees {
    return [self.employees objectEnumerator];
}

• memberOf<Key>:

- (Employee *)memberOfEmployees:(Employee *)anObject {
    return [self.employees member:anObject];
}
可变无序集合

支持一对多可变关系的无序访问需要实现额外的访问方法。实现这些可变无序访问方法允许你响应mutableSetValueForKey:方法并返回可变无序集合代理对象。实现这些访问方法比依赖一个直接返回可变对象来改变数据更高效。这个一对多可变关系也符合KVO。

为了实现可变有序的一对多关系,实现下面额外方法:

• add<Key>Object:或者add<Key>:

- (void)addEmployeesObject:(Employee *)anObject {
    [self.employees addObject:anObject];
}
 
- (void)addEmployees:(NSSet *)manyObjects {
    [self.employees unionSet:manyObjects];
}

• remove<Key>Object:或者remove<Key>:

- (void)removeEmployeesObject:(Employee *)anObject {
    [self.employees removeObject:anObject];
}
 
- (void)removeEmployees:(NSSet *)manyObjects {
    [self.employees minusSet:manyObjects];
}
• intersect<Key>:

可选地提供这个方法可以提高性能。

- (void)intersectEmployees:(NSSet *)otherObjects {
    return [self.employees intersectSet:otherObjects];
}


处理非对象值

通常,KVC默认实现会包装和解包装非对象值。但是你可以处理非对象属性设置nil值的情况。

这种情况下,KVC默认实现会调用setNilValueForKey:方法并抛出NSInvalidArgumentException异常,你可以在子类重写这个方法来处理这种情况。

- (void)setNilValueForKey:(NSString *)key
{
    if ([key isEqualToString:@"age"]) {
        [self setValue:@(0) forKey:@”age”];
    } else {
        [super setNilValueForKey:key];
    }
}


添加验证

KVC协议定义使用键或者键路径来验证属性的方法。默认实现依赖于你实现的validate<Key>:error:方法;这个key对应属性名。

如果你没有实现属性的验证方法,默认实现会认为验证成功。这意味着你可以可选的为某个属性添加验证。

实现验证方法

当你提供属性的验证方法,这个方法接收2个参数的引用:需要验证的值对象和用于返回错误信息的错误对象。你的验证方法可能会执行3种行为:

• 当值对象是有效的,返回YES而且不修改值对象或者错误对象。

• 当值对象是无效的而且你不想提供另一个有效的值,返回NO并提供一个NSError对象来指示失败原因。

• 当值对象是无效的,但你可以提供一个有效的值,创建有效对象并赋值给值对象引用,返回YES并不改变错误对象。如果你提供另一个值对象,你应该总是返回新的值对象而不是修改原来需要验证的值对象,即使这个值对象是可变的。

- (BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError{
    if ((*ioValue == nil) || ([(NSString *)*ioValue length] < 2)) {
        if (outError != NULL) {
            *outError = [NSError errorWithDomain:PersonErrorDomain
                                            code:PersonInvalidNameCode
                                        userInfo:@{ NSLocalizedDescriptionKey
                                                    : @"Name too short" }];
        }
        return NO;
    }
    return YES;
}
验证数值
- (BOOL)validateAge:(id *)ioValue error:(NSError * __autoreleasing *)outError {
    if (*ioValue == nil) {
        // Value is nil: Might also handle in setNilValueForKey
        *ioValue = @(0);
    } else if ([*ioValue floatValue] < 0.0) {
        if (outError != NULL) {
            *outError = [NSError errorWithDomain:PersonErrorDomain
                                            code:PersonInvalidAgeCode
                                        userInfo:@{ NSLocalizedDescriptionKey
                                                    : @"Age cannot be negative" }];
        }
        return NO;
    }
    return YES;
}


描述属性关系

类描述提供一个方法来描述一对一和一对多关系属性。定义这些属性的关系允许KVC更聪明和灵活地处理。

类描述

NSClassDescription是一个提供获取类的元数据接口的基类。类描述对象记录可用属性(attribute)和这个类的对象与其他对象的关系(一对一,一对多,或者相反)。例如attributeKeys方法返回类定义的一组属性;toManyRelationshipKeys方法和toOneRelationshipKeys方法返回一组一对一关系的键和一对多关系的键;inverseRelationshipKey方法返回提供键的相反方向的关系。

NSClassDescription没有定义定义关系的方法。具体的子类必须定义这些方法。一旦创建,你必须使用NSClassDescription的registerClassDescription:forClass:类方法来注册类描述的类。

NSScriptClassDescription是cocoa中唯一提供的具体子类,这封装了应用的脚本信息。


性能设计

KVC是高效的,特别是当你使用默认实现来完成大量的工作,但是这会比直接调用方法稍微慢些。只有在使用KVC可以提高灵活性的情况下使用或者在其他cocoa技术依赖KVC时使用。

重写KVC方法

通常,你只需要继承NSObject来获取KVC特性并定义符合KVC命名的属性和访问方法。你可能需要重写KVC默认实现的访问方法,例如valueForKey:和setValue:forKey:或者基于键的验证方法validateValue:forKey:。因为这些实现会缓存运行时环境的信息来提高效率。如果你重写它们来添加额外的逻辑,确保你在运行前调用父类的默认实现。

优化一对多关系

当你实现一对多关系,在很多情况下,有序形式的访问方法提供重要的性能,特别对于可变集合。


符合KVC约定的检查表

下面的总结确保你符合KVC约定。

属性(attribute)和一对一关系的符合

对于每一个属性或者一对一关系的属性:

√实现<key>或者is<Key>方法,或者创建<key>或者is<Key>实例变量。当自动同步属性时,编译器通常会帮你完成。

注意:通常属性名称的命名为小写开头,默认实现同样可以处理大写开头的属性,例如URL。

√如果属性是可变的,实现set<Key>:方法。当自动同步属性时,编译器通常会帮你完成。

注意:当你重写默认的设置方法,确保不要调用任何的属性验证方法。

√如果属性值是数值,重写setNilValueForKey:方法来处理设置nil值给非对象属性。

有序一对多关系的符合

对于每一个有序一对多关系(例如NSArray):

√实现<key>方法来返回数组或者有一个<key>或者_<key>的数组实例变量。当自动同步属性时,编译器通常会帮你完成。

√另外的,实现countOf<Key>和objectIn<Key>AtIndex:和<key>AtIndexes:的其中一个。

√可选的,实现get<Key>:range:方法来提高性能。

如果属性是可变的,还要实现:

√实现一个或者同时两个insertObject:in<Key>AtIndex:和insert<Key>:atIndexes:方法。

√实现一个或者同时两个removeObjectFrom<Key>AtIndex:和remove<Key>AtIndexes:方法。

√可选的,实现replaceObjectIn<Key>AtIndex:withObject:或者replace<Key>AtIndexes:with<Key>:方法来提高性能。

无序一对多关系的符合

对于每一个无序一对多关系(例如NSSet):

√实现<key>方法来返回NSSet实例或者有一个<key>或者_<key>的NSSet实例变量。当自动同步属性时,编译器通常会帮你完成。

√另外的,实现countOf<Key>,enumeratorOf<Key>,memberOf<Key>:方法。

如果属性是可变的,还要实现:

√实现一个或者同时两个add<Key>Object:和add<Key>:。

√实现一个或者同时两个remove<Key>Object:和remove<Key>:。

√可选的,实现intersect<Key>:方法来提高性能。

验证

可选地需要验证属性:

√实现validate<Key>:error:方法,返回布尔值来指示值对象是否有效和错误对象的引用。

相关标签: KVC iOS