Objective-C 之 KVC 底层原理

214 阅读24分钟

KVC 简介

KVC(Key-value coding)键值编码,是由NSKeyValueCoding非正式协议启用的一种机制,对象采用这种机制来提供对其属性的间接访问。当对象符合 KVC 时,可通过 Key 来访问其属性。这种间接访问机制补充了实例变量及其关联的访问器方法提供的直接访问。KVC 是在运行时动态地访问和修改对象的属性。

KVC 是许多其他Cocoa 技术的基础,例如KVO。在某些情况下,键值编码还可以帮助简化代码。

NSObject采用NSKeyValueCoding协议,并为这些和其他必要方法提供了默认实现。所以对于所有继承 NSObject 的对象,都能使用 KVC (一些纯 Swift 类和结构体是不支持 KVC 的,因为没有继承 NSObject) ,执行以下操作:

  • 访问对象属性。
  • 操作集合属性。
  • 在集合对象上调用集合运算符。
  • 访问非对象属性。
  • 通过键路径访问属性。

访问对象的属性

KVC主要对三种类型进行操作,基础数据类型及常量、对象类型、集合类型。

实例: BankAccount声明的对象的属性。

@interface Person : NSObject 

@property (nonatomic, copy) NSString *name;

@end

@interface Transaction : NSObject
 
@property (nonatomic) NSString* payee;   
@property (nonatomic) NSNumber* amount;  
@property (nonatomic) NSDate* date;      
 
@end
  
@interface BankAccount : NSObject
 
@property (nonatomic) NSNumber *currentBalance;              
@property (nonatomic) Person *owner;                         
@property (nonatomic) NSArray<Transaction *> *transactions; 
 
@end

使用KVC时,直接将属性名当做key

[myAccount setValue:@(100.0) forKey:@"currentBalance"];
NSNumber *curBalance =[myAccount valueForKey:@"currentBalance"]
获取属性值

使用键获取属性值

NSNumber *curBalance = [myAccount valueForKey:@"currentBalance"]

返回由 key 参数命名的属性的值。如果根据访问者搜索模式中描述的规则找不到由 key 命名的属性,则该对象向自身发送valueForUndefinedKey:消息。默认实现valueForUndefinedKey:会引发一个NSUndefinedKeyException,但是子类可以覆盖此行为,并更优雅地处理该情况。

当访问对象是一个集合时,则返回由 key 参数命名的属性值的集合。例如,访问对象是transactionskey 是payee,将返回一个数组,该数组包含transactions(数组)中所有transaction对象的属性payee对象。

NSArray *payees = [myAccount.transactions valueForKey:@"payee"];

使用键路径获取属性值

 NSString *name = [myAccount valueForKeyPath:@"owner.name"]

返回指定 keyPath 的值。keyPath 序列中不符合特定键的键值编码的任何对象(即,其默认实现valueForKey:无法找到访问器方法)都将接收到valueForUndefinedKey:消息。

当使用 key 路径寻址属性时,除 key 路径中的最后一个 key 外,如果有其他 key 是一个集合,则返回的值是一个包含最后一个 key 所有值的集合。例如,获取键路径transactions.payee的值,将返回一个数组,该数组包含transactions(数组)中所有transaction对象的属性payee对象。这也适用于键路径中的多个数组。获取关键路径accounts.transactions.payee的值,将返回一个数组,该数组包含accounts(数组)中transactions(数组)中所有transaction对象的属性payee对象。

//获取数组 transactions 中所有 transaction 对象的属性 payee 对象。
NSArray *payees1 = [myAccount valueForKeyPath:@"transactions.payee"];


//获取数组 accounts 中所有 transactions 中所有 transaction 对象的属性 payee 对象。
NSArray *payees2 = [myBank valueForKey:@"accounts.transactions.payee"];
设置属性值

使用键设置属性值

 [myAccount setValue:@(100.0) forKey:@"currentBalance"];

将访问对象由 key 参数命名的属性的值设置为参数 value 的值。setValue:forKey:自动展开NSNumberNSValue表示标量和结构的对象的默认实现,并将它们分配给属性。如果根据访问者搜索模式中描述的规则找不到由 key 命名的属性,则该对象向自身发送setValue:forUndefinedKey:消息。默认实现setValue:forUndefinedKey:会引发一个NSUndefinedKeyException。但是,并更优雅地处理该情况。

当访问对象是一个集合时,则将访问对象中包含的每一个对象由 key 参数命名的属性的值设置为参数 value 的值。例如,访问者是transactionskey 是payee,则将transactions中包含的每一个transaction对象的属性payee的值设置为参数 value 的值。

//将数组 transactions 中的所有 transaction 对象的 payee 属性 设置为 XXX
[transactions setValue:@"XXX" forKey:@"payee"]

使用键路径设置属性值

 [myAccount setValue:@"WW" forKeyPath:@"owner.name"];

将访问对象的指定参数 keyPath 处设置为参数 value 的值。keyPath 序列中不符合特定键的键值编码的任何对象都会收到一条setValue:forUndefinedKey:消息。

当使用key路径寻址属性时,除 key 路径中的最后一个 key 外,如果有其他 key 是一个集合,则将集合中以最后一个 key 命名的属性的值设置为value。例如,设置键路径transactions.payee的值,将数组transactions中的所有transaction对象的payee属性 设置为value。这也适用于键路径中的多个数组。

//将数组 transactions 中的所有 transaction 对象的 payee 属性 设置为 XXX
[myAccount setValue:@"XXX" forKeyPath:@"transactions.payee"];

//将数组 accounts 中所有的 transactions 数组中的所有 transaction 对象的 payee 属性 设置为 XXX
[myBank setValue:@"XXX" forKeyPath:@"accounts.transactions.payee"];
模型字典转换

KVC里面有两个关于 NSDictionary 的方法:

//根据一组 key,返回这组 key 对应属性的字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

//用一个字典的 key : Value 设置 Model 的属性
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
@inpersonrface Address : NSObject
@end

@inpersonrface Address()
@property (nonatomic, copy)NSString* city;
@property (nonatomic, copy)NSString* country;
@property (nonatomic, copy)NSString* district;
@property (nonatomic, copy)NSString* province;
@end
@implementation Address
@end  
    
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //模型转字典
        Address* add = [Address new];
        add.country = @"China";
        add.province = @"Fu Jian";
        add.city = @"Xia Men";
        add.district = @"Si Ming";
        NSArray* arr = @[@"country",@"province",@"city",@"district"];
      	//模型转字典
        NSDictionary* dict = [add dictionaryWithValuesForKeys:arr]; //把对应key所有的属性全部取出来
        NSLog(@"%@",dict);
        //字典转模型
        NSDictionary* modifyDict = @{@"country":@"USA",@"province":@"california",@"city":@"Los angle"};
        [add setValuesForKeysWithDictionary:modifyDict];//用key Value来修改Model的属性
        NSLog(@"country:%@  province:%@ city:%@",add.country,add.province,add.city);
    }
    return 0;
}

访问集合的属性

可以像其他对象一样使用valueForKey:setValue:forKey:(或KeyPath),获取或设置(非可变)集合对象。但是,要操纵可变集合的内容时,通常使用以下方法。

  • mutableArrayValueForKey:mutableArrayValueForKeyPath:返回NSMutableArray对象。
  • mutableSetValueForKey:mutableSetValueForKeyPath:返回NSMutableSet对象。
  • mutableOrderedSetValueForKey:mutableOrderedSetValueForKeyPath:返回NSMutableOrderedSet`对象

使用以上方法创建可变集合对象,然后通过setValue:forKey:消息将其存储回该对象更为有效。在许多情况下,它比直接使用可变属性更有效。

定义集合方法

当您使用标准命名约定创建访问器和实例变量时,KVC 协议的默认实现可以响应KVC 方法来定位它们。但是,如果实现集合访问器方法而不是集合属性的基本访问器,或者除了实现基本访问器之外,则可以:

  • 使用NSArray或NSSet以外的类对多个关系进行建模。 在对象中实现集合方法时,KVC 获取器的默认实现会返回一个代理对象,该代理对象将调用这些方法以响应NSArrayNSSet的消息。基础属性对象不必是NSArrayNSSet本身,因为代理对象使用您的收集方法提供了默认实现。

  • 更改多对多关系的内容时,可以提高性能。KVC 默认实现不是使用KVC 设置器重复创建新的集合对象来响应每次更改,而是使用定义的集合方法来对基础属性进行适当的改变。

根据您是希望关系表现为索引,有序集合(如NSArray对象)还是无序唯一的集合(如NSSet对象),来实现两种类型的集合访问器之一。在任何一种情况下,您都实现至少一组方法以支持对该属性的读取访问,然后添加另一组以启用对集合内容的更改。

有序集合

可以添加索引访问器方法来提供一种机制,用于计数、检索、添加和替换有序关系中的对象。底层对象通常是NSArrayNSMutableArray的实例,但如果提供集合访问器,则可以将为其实现这些方法的任何对象属性作为数组进行操作。

有序集合获取器 对于没有默认getter的集合属性,如果提供以下索引集合getter方法,则 KVC 的默认实现将响应valueForKey:消息时,返回类似于的代理对象NSArray,,但调用以下集合方法来完成其工作。

  • countOf<Key>

此方法与NSArraycount方法对应返回对象数。当属性为时NSArray,可以使用该方法。

- (NSUInteger)countOfTransactions {
    return [self.transactions count];
}
  • objectIn<Key>AtIndex:<key>AtIndexes:

    返回位于指定索引处的对象。或, 返回一个数组,该数组中的对象位于给定索引集指定的索引处。分别对应于NSArray方法objectAtIndex:objectsIndexes:。实现其中之一即可

//返回对象在多对多关系中指定索引处的对象
- (id)objectInTransactionsAtIndex:(NSUInteger)index {
    return [self.transactions objectAtIndex:index];
}
 
//返回NSIndexSet参数指定的索引处对象的数组。
- (NSArray *)transactionsAtIndexes:(NSIndexSet *)indexes {
    return [self.transactions objectsAtIndexes:indexes];
}
  • get<Key>:range:

    此方法是可选的,但可以提高性能。与NSArray方法相对应的对象getObjects:range:。它返回集合中属于指定范围内的对象。

- (void)getTransactions:(Transaction * __unsafe_unretained *)buffer
               range:(NSRange)inRange {
    [self.transactions getObjects:buffer range:inRange];
}

访问可变有序集合

使用索引访问器支持可变到多关系,需要实现一组不同的方法。当您提供这些setter方法时,默认实现将响应该mutableArrayValueForKey:消息,返回类似于NSMutableArray对象的代理对象,但使用该对象的方法来完成其工作。通常,这比NSMutableArray直接返回对象更有效。

为了使您的对象键值编码符合可变的有序一对多关系,请实现以下方法:

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

在给定索引处将给定对象插入数组的内容中。或,将所提供数组中的对象插入指定索引处的接收数组中。这些类似于NSMutableArray方法insertObject:atIndex:insertObjects:atIndexes:。这些方法只需要一种。

对于声明为NSMutableArraytransactions对象:

- (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:

删除索引处的对象。或,从数组中移除指定索引处的对象。此方法类似于removeObjectAtIndex:,但允许通过一个操作高效地删除多个对象这些方法对应NSMutableArray的方法removeObjectAtIndex:removeObjectsAtIndexes:。这些方法只需要一种。

- (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>:

将索引处的对象替换为指定对象。或,替换接收数组中由给定数组中的对象指定的位置处的对象。它们对应于NSMutableArray方法replaceObjectAtIndex:withObject:replaceObjectsAtIndexes:withObjects:。在对应用程序进行性能分析时发现性能问题时,可以选择提供这些方法。

- (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];
}
无序集合

定义无序集合访问器方法,以提供一种机制来访问和改变无序关系中的对象。通常,此关系是NSSetNSMutableSet对象的实例。但是,在实现这些访问器时,可以使任何类对关系进行建模,并使用 KVC 对其进行操作,就像它是的实例一样NSSet

无序集合获取器

定义以下集合getter方法以返回集合中的对象数,遍历集合对象并测试对象是否已存在于集合中时,KVC 的默认实现会在响应valueForKey:消息时返回行为类似于的代理对象NSSet,但是调用以下收集方法来完成其工作。

  • countOf<Key>

此方法与NSSetcount方法对应, 返回集合中的对象数。当对象是时NSSet,可以直接调用此方法。

- (NSUInteger)countOfEmployees {
    return [self.employees count];
}
  • enumeratorOf<Key>

此方法与NSSet方法objectEnumerator对应,返回一个NSEnumerator实例,该实例用于迭代NSSet中的枚举器对。

- (NSEnumerator *)enumeratorOfEmployees {
    return [self.employees objectEnumerator];
}
  • memberOf<Key>

确定给定对象是否存在于集中,如果存在,则返回该对象。否则返回nil。如果手动实现比较,则通常使用isEqual:来比较对象。当对象NSSet对象时,可以使用等效的member:方法:

- (Employee *)memberOfEmployees:(Employee *)anObject {
    return [self.employees member:anObject];
}

可变无序集合

可变无序一对多关系的属性使用访问器,需要实现可变的无序访问器,以允许对象根据该mutableSetValueForKey:方法提供无序的集合代理对象。实现这些访问器比依赖访问器直接返回可变对象以对关系中的数据进行更改要高效得多。

为了对可变的无序一对多关系的进行键值编码投诉,请实施以下方法:

  • add<Key>Object:add<Key>:

将给定对象添加到集合(如果它还不是成员)或将另一个给定集中的每个对象添加到接收集中(如果不存在)。这些类似于NSMutableSet方法addObject:unionSet:。这些方法只需要一种。

这些方法将单个对象或一组对象添加到集合中。向集合添加一组对时,请确保集合中尚未存在等效对象。这些类似于NSMutableSet方法addObject:unionSet:。这些方法只需要一种。

- (void)addEmployeesObject:(Employee *)anObject {
    [self.employees addObject:anObject];
}
 
- (void)addEmployees:(NSSet *)manyObjects {
    [self.employees unionSet:manyObjects];
}
  • remove<Key>Object:remove<Key>:

从集合中移除单个对象或或从接收集中移除另一个给定集中的每个对象(如果存在)。。它们类似于NSMutableSet方法removeObject:minusSet:。这些方法只需要一种。

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

此方法接收NSSet参数,从接收集中删除不是另一个给定集成员的每个对象。这相当于该NSMutableSet方法intersectSet:。当分析表明与集合内容更新有关的性能问题时,可以选择实现此方法。

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

使用集合运算符

当您发送键值编码兼容对象valueForKeyPath:message时,您可以在键路径中嵌入一个集合运算符。集合运算符是前面有at符号(@)的小关键字列表之一,它指定getter应该执行的操作,以便在返回数据之前以某种方式操作数据。NSObject提供的valueForKeyPath:的默认实现实现实现了此行为。

valueForKeyPath:方法非常强大,可以在键路径中嵌入集合运算符。该运算符指定getter在返回数据之前执行某种。

当键路径包含集运算符时,该运算符前面的键路径的任何部分(称为左键路径)指示相对于消息接收方要操作的集合。如果将消息直接发送到集合对象(如NSArray实例),则可能会省略左键路径。

运算符后面的键路径部分(称为右键路径)指定了该运算符应处理的集合中的属性。除@count之外的所有集合运算符都需要一个正确的键路径。

操作键路径格式

集合运算符主要分为三类:

  1. 集合操作符:返回一个与右键路径中指定的属性的数据类型匹配的单个(NSNumber)对象。@count除外。
  2. 数组操作符:根据操作符的条件,将符合条件的对象包含在数组中返回。
  3. 嵌套操作符:处理集合对象中嵌套其他集合对象的情况,返回结果也是一个集合对象。
集合操作符

**@avg **

获取集合中right keyPath指定的属性的平均值。

指定@avg运算符时,将为集合的每个元素读取由右键路径指定的属性,将其转换为double(用0代替nil值),并计算这些值的平均值。然后,它返回存储在NSNumber实例中的结果。

//获取 transactions 数组中所有 transaction 的 amount 平均值
NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];

**@count **

获取集合中对象的数量。

指定@count运算符时,返回NSNumber实例中集合中的对象数。右键路径(如果存在)将被忽略。

//获取 transactions 数组中 Transaction 对象的数量
NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@"@count"];

@max

获取集合中right keyPath指定的属性的最大值。

指定@max运算符时,在集合所有对象中搜索并返回由右键路径命名的属性的最大值。搜索使用compare:方法进行比较,该许多Foundation类,(如NSNumber类)定义。因此,右键路径所指示的属性必须能响应compare:方法的对象。

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

@min

获取集合中right keyPath指定的属性的最小值。

指定@min运算符时,在集合所有对象中搜索并返回由右键路径命名的属性的最大值。搜索使用compare:方法进行比较,该许多Foundation类,(如NSNumber类)定义。因此,右键路径所指示的属性必须能响应compare:方法的对象。

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

备注:@max@min在进行判断时,都是通过调用compare:方法进行判断,所以可以通过重写该方法对判断过程进行控制。

@sum

获取集合中right keyPath指定的属性的总和。

指定@sum运算符时,将为集合的每个元素读取由右键路径指定的属性,将其转换为double(用0代替nil值),并计算这些值总和。然后,它返回存储在NSNumber实例中的结果。

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

@distinctUnionOfObjects

创建并返回一个数组,将集合对象中,所有right keyPath指定的属性放在一个数组中,并将数组进行去重后返回。

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

@unionOfObjects

创建并返回一个数组,将集合对象中,所有right keyPath指定的属性放在一个数组返回,而不会删除重复项。

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

注意:以上两个方法中,如果操作的属性为nil,在添加到数组中时会导致Crash

嵌套操作符

嵌套运算符对嵌套集合进行操作,其中集合的每个元素本身是一个集合。

@distinctUnionOfArrays

创建并返回一个数组,将集合对象中所有集合对象的所有right keyPath指定的属性放在一个数组中,并将数组进行去重后返回。

//accounts 数组中嵌套 transactions 数组
NSArray *collectedDistinctPayees = [accounts valueForKeyPath:@"@distinctUnionOfArrays.payee"];

@unionOfArray

创建并返回一个数组,将集合对象中所有集合对象的所有right keyPath指定的属性放在一个数组中返回,而不会删除重复项。

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

@distinctUnionOfSets

创建并返回一个NSSet对象,将集合对象中所有集合对象的所有right keyPath指定的属性放在一个NSSet对象中,并将NSSet对象进行去重后返回。

该运算符的行为类似于@distinctUnionOfArrays,不同之处在于它期望NSSet包含NSSet对象实例而不是NSArray实例实例的NSArray实例。同样,它返回一个NSSet实例。假设示例数据已存储在集合中而不是数组中,则示例调用和结果与中显示的相同@distinctUnionOfArrays

非对象值处理

通过NSObject使用对象和非对象属性提供的 KVC 方法的默认实现。默认实现自动在对象参数或返回值与非对象属性之间转换。即使属性是础数据类型或结构体。

使用 KVC 获取属性值时, 总是返回一个id对象,如果原本属性类型是值类型或者结构体,则 KVC 使用此值初始化一个NSNumber对象(用于标量)或NSValue对象(用于结构),然后返回该值。但是使用 KVC 设置属性值时。必须手动将值类型转换成NSNumber或者NSValue类型,因为传递进去和取出来的都是OC对象,所以需要开发者自己担保类型的正确性,运行时 Objective-C 在发送消息的会检查类型,如果错误会直接抛出异常。

注意:

当使用 KVC 设置非对象属性值传入nil时,则该对象向自身发送setNilValueForKey:。此方法的默认实现引发NSInvalidArgumentException异常,但子类可能会覆盖此行为,如处理非对象值中所述,例如: 设置标记值或提供有意义的默认值。

例如:将nil值写入非对象(Bool hidden;CGFloat age;)属性,可以进行覆盖setNilValueForKey:以处理这种情况,hidden属性将nil解释为NOage属性将nil解释为 0 :

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

验证属性

KVC 定义了用于通过键或键路径验证属性的方法。这些方法的默认实现依赖于按照与访问器方法相似的命名模式定义方法。具体地说,可以为任何要验证的名为Key的属性提供validate<Key>:error:方法。默认实现会对此进行搜索,以响应键 KVCvalidateValue:forKey:error:消息。

调用validateValue:forKey:error:(或validateValue:forKeyPath:error:)方法时,默认实现会在接收验证消息的对象(或键路径末尾的对象)中搜索与名称匹配的验证方法validate<Key>:error:。如果不为属性提供验证方法,则默认该属性验证成功并返回YES,而不管值是什么。这意味着您可以逐个属性选择验证。当存在指定的属性的验证方法时,默认实现将返回调用该方法的结果。

为属性提供验证方法时,该方法通过引用接收两个参数:要验证的值对象和NSError用于返回错误信息的值。结果,您的验证方法可以采取以下三种操作之一:

  • 当值对象有效时,返回YES并不更改值对象或错误。
  • 当值对象无效时,并且没有提供有效的替代方法时,请将error参数设置为一个NSError指示失败原因并返回的对象NO
  • 当值对象无效但您知道有效的替代方法时,但创建一个新的有效对象作为替换。在这种情况下,该方法返回YES,同时保持NSError对象不变。在返回之前,该方法将值引用修改为指向新值对象。当它进行修改时,该方法总是创建一个新对象,而不是修改旧对象,即使值对象是可变的。

验证属性示例:

Person* person = [[Person alloc] init];
NSError* error;
NSString* name = @"John";
//在validateValue方法的内部实现中,如果传入的value或key有问题,可以通过返回NO来表示错误,并设置NSError对象。
if (![person validateValue:&name forKey:@"name" error:&error]) {
    NSLog(@"%@",error);
}

validate<Key>:error:

定义validate<Key>:error:格式的方法,并在方法内部实现验证代码。

- (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;
}

非对象属性的验证

验证方法期望value参数是一个对象,结果,非对象属性的值将包装在NSValueNSNumber对象中。

- (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;
}
#import <Foundation/Foundation.h>

@inpersonrface Person: NSObject {
    NSUInpersonger _age;
}

@end

@implementation Person

- (BOOL)validapersonValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError {
    NSNumber *age = *ioValue;
    if (age.inpersongerValue == 10) {
        return NO;
    }
    
    return YES;
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
      
        //Test生成对象
        Person *personst = [[Test alloc] init];
        //通过KVC设值personst的age
        NSNumber *age = @10;
        NSError* error;
        NSString *key = @"age";
        BOOL isValid = [personst validapersonValue:&age forKey:key error:&error];
        if (isValid) {
            NSLog(@"键值匹配");
            [personst setValue:age forKey:key];
        }
        else {
            NSLog(@"键值不匹配");//键值不匹配 
        }
        //通过KVC取值age打印
        NSLog(@"person的年龄是%@", [personst valueForKey:@"age"]);  
      	//person 的年龄是0
    }
    return 0;
}

这样就给了我们一次纠错的机会。需要指出的是,KVC是不会自动调用键值验证方法的,就是说我们如果想要键值验证则需要手动验证。但是有些技术,比如CoreData会自动调用。

访问搜索模式

KVC 默认实现使用 key 在自己的对象中搜索访问器,实例变量以及遵循 KVC 命名约定的相关方法。

取值搜索模式

valueForKey:的默认实现搜索过程如下:

  1. 按照顺序搜索对象中get<Key><key>is<Key>_<key>的方法,找到直接调用。

  2. 若未找到简单的访问器方法,则搜索与名称匹配的countOf<Key>objectIn<Key>AtIndex:(与NSArray该类定义的原始方法<key>AtIndexes:相对应)和(与该NSArray方法相对应)相匹配的方法objectsAtIndexes:

    若找到第一个以及其他两个中的至少一个,则创建一个响应所有NSArray方法的代理对象并将其返回。否则,继续到第三步。

    代理对象随后将NSArray接收到的countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes:消息给符合KVC规则的调用对象。

  3. 若未找到简单的访问器方法或数组访问方法组,则搜索名为countOf<Key>enumeratorOf<Key>memberOf<Key>:的方法(对应于NSSet类定义的原始方法)。

    若找到所有三个方法,创建一个响应所有NSSet方法的代理对象并将其返回。否则,请继续执行步骤4。

    此代理对象随后将它接收到的任何NSSet消息转换为countOf<Key>enumeratorOf<Key>memberOf<Key>消息的组合,发送给创建它的对象。实际上,代理对象与 符合 KVC 的对象一起使用,允许基础属性的行为如同NSSet,即使它不是NSSet

  4. 若未找到简单的访问器方法或集合访问方法组,且接收方的类方法accessInstanceVariables返回YES,则按顺序搜索名为_key, _isKey , key ,isKey 的实例变量。如果找到,直接获取实例变量的值。

  5. 如果其他所有方法均失败,那么会直接调用valueForUndefinedKey:。默认情况下会引发异常。

设值搜索模式

setValue: forKey:的默认实现搜索过程如下:

  1. 按照顺序搜索对象中setKey:_setKey: 、setIsKey:`的方法,找到直接调用。
  2. 若未找到简单的访问器方法, 且接收方的类方法accessInstanceVariables返回YES, 则按顺序搜索名为_key, _isKey , key ,isKey 的实例变量。如果找到,直接获取实例变量的值。
  3. 若未找到访问器或实例变量,则调用setValue:forUndefinedKey:。默认情况下会引发异常。

实现 KVC 合规性

让对象继承NSObject,通过使用@property语句声明一个属性,并允许编译器自动合成ivar和访问器,来遵守Objective-C中的标准模式。默认情况下,编译器遵循预期的模式。

KVC 使用场景

KVC 在 iOS 开发中是绝不可少的利器,这种基于运行时的编程方式极大地提高了灵活性,简化了代码,甚至实现很多难以想像的功能,KVC也是许多iOS开发黑魔法的基础。

动态地取值和设值

利用 KVC 动态的取值和设值是最基本的用途。

访问和修改私有变量

对于类里的私有属性,Objective-C 是无法直接访问,但是 KVC 是可以。

Model 和字典转换

KVC 强大作用的又一次体现,KVC 和 Objc 的 runtime 组合可以很容易的实现 Model 和字典的转换。

修改控件的内部属性

iOS 开发中必不可少的小技巧。众所周知很多 UI 控件都由很多内部 UI 控件组合而成的,但是 Apple 没有提供这访问这些控件的API,这样我们就无法正常地访问和修改这些控件的样式。而 KVC 在大多数情况可下可以解决这个问题。最常用的就是个性化 UITextField中的 placeHolderText。

操作集合

Apple对KVC的valueForKey:方法作了一些特殊的实现,比如说NSArray和NSSet这样的容器类就实现了这些方法。所以可以用KVC很方便地操作集合。

实现高阶消息传递

当对容器类使用KVC时,valueForKey:将会被传递给容器中的每一个对象,而不是容器本身进行操作。结果会被添加进返回的容器中,这样,开发者可以很方便的操作集合来返回另一个集合

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSArray* arrStr = @[@"english",@"franch",@"chinese"];
        NSArray* arrCapStr = [arrStr valueForKey:@"capitalizedString"];
      
        for (NSString* str  in arrCapStr) {
            NSLog(@"%@",str);
        }
      
        NSArray* arrCapStrLength = [arrStr valueForKeyPath:@"capitalizedString.length"];
      
        for (NSNumber* length  in arrCapStrLength) {
            NSLog(@"%ld",(long)length.inpersongerValue);
        }  
    }
    return 0;
}

方法capitalizedString被传递到NSArray中的每一项(字符串对象),这样,NSArray的每一员都会执行capitalizedString并返回一个包含结果的新的NSArray。所有String都成功以转成了大写。

同样如果要执行多个方法也可以用valueForKeyPath:方法。它先会对每一个成员调用 capitalizedString方法,然后再调用length,因为length方法返回是一个数字,所以返回结果以NSNumber的形式保存在新数组里.

实现 KVO

​ KVO是基于KVC实现的