KVC

340 阅读7分钟

今天整理了一下有关KVC的知识,参考苹果文档

KVC的基本用法

访问对象的属性

赋值

这里有三种类型。属性,一对一关系,一对多关系。如下

@interface BankAccount : NSObject
 
@property (nonatomic) NSNumber* currentBalance;              // 属性
@property (nonatomic) Person* owner;                        // 一对一
@property (nonatomic) NSArray< Person* >* transferPerson;   // 一对多
 
@end

分别用KVC给三个属性赋值

  1. 直接的属性可以用setValue:forKey:
  2. 如果属性对象包含的属性(比如设置owner.name),可以用setValue:forKeyPath: 3)setValuesForKeysWithDictionary:。通过传递的字典,遍历所有的keys,调用setValue:forKey:来给对象赋值。 代码如下:
    //设置利率
    [account setValue:@(4.5) forKey:@"currentBalance"];
    //设置户主的名字(Person类里面有name属性)
    [account setValue:@"Jack" forKeyPath:@"owner.name"];
    //设置户主家所在的城市(Person类里面有个Address *address)
    [account setValue:@"Beijing" forKeyPath:@"owner.address.city"];
    //设置户主所有转账人的名字。
    [account setValue:@"transferPerson" forKeyPath:@"transferPerson.name"];
    
    //通过字典给account赋值
    Person *person = [Person new];
    person.name = @"rose";
    [account setValuesForKeysWithDictionary:@{@"currentBalance":@(4.5),@"owner":person}];

取值

  1. valueForKey: 和 valueForKeyPath: 来取值。
  2. dictionaryWithValuesForKeys: 这个方法会遍历传进来的keys数组,调用valueForKey:,得到的value和key添加到字典中,最终返回此字典。 代码如下:
    //取利率
    NSNumber *currentBalance = [account valueForKey:@"currentBalance"];
    //取户主的姓名
    NSString *name = [account valueForKeyPath:@"owner.name"];
    //取户主的所有转账人的名字
    NSArray *transferNames = [account valueForKeyPath:@"transferPerson.name"];
    //通过一个包含放key的数组,得到key:value字典。比如此值为{@"currentBalance":@(4.5)}
    NSDictionary *transferDic = [account dictionaryWithValuesForKeys:@[@"currentBalance"]];

此外,如果想要操作一些集合类属性的内容,需要返回一可变类型的,可用以下方法

  1. (NSMutableArray有序数组)mutableArrayValueForKey: 和 mutableArrayValueForKeyPath:
  2. (NSMutableSet无序集合、元素不重复)mutableSetValueForKey: 和 mutableSetValueForKeyPath:
  3. (NSMutableOrderedSet有序集合,元素不重复)mutableOrderedSetValueForKey: 和 mutableOrderedSetValueForKeyPath:

使用运算符

集合运算符

  1. 取平均数(@avg)、求各(@sum)
//计算转账人的平均年龄。如果有3个转账人,年龄分别是11,22,33
NSNumber *avgAge = [account.transferPerson valueForKeyPath:@"@avg.age"];//得出22
//年龄和
NSNumber *sumAge = [account.transferPerson valueForKeyPath:@"@sum.age"];//得出66

先获取集合里面的每一元素,转成double类型(我试了字符串会崩溃,应该是包含NSNumber类型和CGFloat这种类型。nil类型会转成0),然后计算平均值或者和。结果会被存在NSNumber里面返回。 2) 获取集合的个数(@count)

//计算转账人的个数。即返回数组的个数3。
NSNumber *count = [account.transferPerson valueForKeyPath:@"@count"];
  1. 获取集合中的最大值(@max)和最小值(@min)
//获取最大值
NSNumber *maxAge = [account.transferPerson valueForKeyPath:@"@max.age"];
//获取最小值
NSNumber *minAge = [account.transferPerson valueForKeyPath:@"@min.age"];

通过compare:来比较。且会忽略掉nil。即如果有一转账的人没有年龄,minAge不为0,而是返回已知年龄里的最小值。 4)

数组运算符

  1. 删除数组中重复的值(@distinctUnionOfObjects)
//将上面的转账人年龄改成22,22,33,则ageArr返回@[22, 33]
NSArray *ageArr = [account.transferPerson valueForKeyPath:@"@distinctUnionOfObjects.age"];
  1. 显示数组中所有的值(@unionOfObjects)
//这个和下面那个结果显示一样。
NSArray *ageArr = [account.transferPerson valueForKeyPath:@"@unionOfObjects.age"];
NSArray *ageArr = [account.transferPerson valueForKey:@"age"];

KVC访问搜索模式

通过valueForKey:来取值

执行步骤如下:

1、 先找可以访问到此实例的方法。

  1. 名字类似下面的方法,顺序如下:
get<Key> -> <key> -> is<Key> -> _<key>

这里的key是有大小写之分的,is和get开头的方法跟着的key是大写开头的,而key和_key是跟key一样的。也就是说会按以下顺序来寻找方法:

//如果key是name
getName: -> name: -> isName: -> _name:
//如果key是Name(当然一般命名都以小写字母开头)
getName: -> Name: -> isName: -> _Name:
//如果key是_name
get_name: -> _name: -> is_name: -> __name:
//如果key是_Name
get_Name: -> _Name: -> is_Name: -> __Name:

如果没找到,则继续按下面方法来查找方法。

  1. 查找名字类似下面的方法,以name为例:

官方文档上说是会返回一个可以响应NSArray所有方法的代理集合。按下面这种写法,返回的值是NSKeyValueArray类型的。

//countOf<Key> 和 (objectIn<Key>AtIndex:和<key>AtIndexes:的一种)。
//如果后面这两个方法都实现,则只走objectIn<Key>AtIndex:这个方法。

//返回这个数组的个数,此处默认写了2。
- (NSUInteger)countOfName {
    return 2;
}

//遍历count次(本demo中是2次),返回每个index的值。即此处最终得到结果为@[@[@1,@2],@[@1,@2]]
- (id)objectInNameAtIndex:(NSInteger)index {
    return @[@1,@2]; 
}

//也是遍历count次,不过取值和返回的值和数组的objectsAtIndexes:方法一样。结果是@[@1,@2]
- (NSArray *)currentNameAtIndexes:(NSIndexSet *)indexSet {
    return [@[@1,@2] objectsAtIndexes:indexSet];
}

//这个方法可写可不写,能提高性能。
//它从集合中返回属于指定范围内的对象,并与NSArray方法getObjects:range:相对应。
//实践中发现如果写了这个方法,遍历count次的就不走了,否则那个遍历次数会被调用count次(本demo中就是2次)。
- (void)getName:(id __unsafe_unretained *)buffer
               range:(NSRange)inRange {
    [@[@1,@2] getObjects:buffer range:inRange];
}

如果没找到,则继续按下一方法来查找方法。

  1. 查找下面的方法,以name为例:

官方文档上说是返回一个可以响应NSSet所有方法的代理集合。按下面这种写法,返回的值是NSKeyValueSet类型的。

//countOf<Key> 和 enumeratorOf<Key> 和 memberOf<Key>: 三种方法都实现

//返回集合的个数。此处返回了固定值3。
- (NSUInteger)countOfCurrentBalance {
    return 3;
}

//返回一个迭代器类型。不知道可以返回别的不、
- (id)enumeratorOfCurrentBalance {
    return [@[@1,@3,@"我们"] objectEnumerator];
}

//这个也必须写。判断set里面是否有anObject,有的话返回该对象,没有返回nil。
- (id)memberOfEmployees:(id)anObject {
    NSSet *set = [NSSet setWithArray:@[@1,@3,@"我们"]];
    return [set member:anObject];
}

如果这些方法都没有找到,则执行下一步。

2、如果没找到方法,就找实例变量。

  1. 检查类方法accessInstanceVariablesDirectly 是否返回YES。默认是返回YES。
  2. 如果返回NO,则终止此查找。抛出valueForUndefinedKey异常(第3步)。
  3. 如果返回YES,则按下面顺序开始找。
_<key> -> _is<Key> -> <key> -> is<Key>

同上,这里key也是有大小写之分,也就是说会按以下顺序来寻找成员变量:

//如果key是name
_name -> _isName -> name -> isName
//如果key是Name(当然一般命名都以小写字母开头)
_Name -> _isName -> Name -> isName
//如果key是_name
__name -> _is_name -> _name -> is_name
//如果key是_Name
__Name -> _is_Name -> _Name -> is_Name

如果实例变量也没有找到,则会抛出异常也就是下步。

3、异常处理。

可以通过重写valueForUndefinedKey:方法,来处理异常。

- (id)valueForUndefinedKey:(NSString *)key {
    //可以对某个key做单独处理
    if ([key isEqualToString:@"Name"]) {
        return _name;//_name为此类的一实例变量
        return @"123";//固定字符串,如果没找到,通过valueForKey得到的Name永远是123。
    }
    
    return nil;
}

通过setValue:forKey:来赋值。

1、先找set方法,按以下顺序:

set<Key> -> _set<Key>
//比如name。则会调用setName: -> _setName:

如果找不到方法,执行下一步,找实例变量。

2、找实例变量

  1. 检查类方法accessInstanceVariablesDirectly 是否返回YES。默认是返回YES。
  2. 如果返回NO,则终止此查找。setValue:forUndefinedKey:(第3步)。
  3. 如果返回YES,则按下面顺序开始找。
_name -> _isName -> name -> isName

如果没找到,就会抛出异常,也就是下一步。

3、异常处理。

如果是没有找到上面的变量,则可以通过重写setValue:forUndefinedKey:方法解决。

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    if ([key isEqualToString:@"name"]) {
        //做一些处理
    }
}

如果是设置了nil导致的崩溃(给非对象的属性赋值,比如BOOL类型的)。则可以通过重写setNilValueForKey:来处理。

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

通过mutableArrayValueForKey:来取值等

//插入
insertObject:in<Key>AtIndex: or insert<Key>:atIndexes:
//移除
removeObjectFrom<Key>AtIndex: or remove<Key>AtIndexes:
//替换
replaceObjectIn<Key>AtIndex:withObject: or replace<Key>AtIndexes:with<Key>:

获取到的可变数组,通过insert、remove、replace方法可直接对数组操作,不需要改完再存一次。代码如下(以transactions为例,_transactions为成员变量):

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

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

- (void)replaceObjectInTransactionsAtIndex:(NSUInteger)index
                             withObject:(id)anObject {
    [_transactions replaceObjectAtIndex:index
                              withObject:anObject];
}
 
- (void)replaceTransactionsAtIndexes:(NSIndexSet *)indexes
                    withTransactions:(NSArray *)transactionArray {
    [_transactions replaceObjectsAtIndexes:indexes
                                withObjects:transactionArray];
}

用法:

//获取account的成员变量transactions的值。
NSMutableArray *array = [account mutableArrayValueForKey:@"transactions"];
//在index=1处插入@3
[array insertObject:@3 atIndex:1];//此时account的transactions[1]已变成3。

KVC验证方法

格式如下:

- (BOOL)validate<Key>:(id *)ioValue error:(NSError * __autoreleasing *)outError

可用来验证属性的值是否合理。比如age,在Person类里面实现下面方法

- (BOOL)validateAge:(id *)ioValue error:(NSError * __autoreleasing *)outError {
    if (*ioValue == nil) {
        *ioValue = @(0);
    } else if ([*ioValue floatValue] < 0.0) {
        if (outError != NULL) {
            *outError = [NSError errorWithDomain:@"PersonErrorDomain"
                                            code:1011
                                        userInfo:@{ NSLocalizedDescriptionKey
                                                    : @"年龄不能小于0" }];
        }
        return NO;
    }
    return YES;
}

验证的代码如下:

    NSNumber *age = @(account.owner.age);
    NSError* error;
    BOOL result = [account validateValue:&(age) forKeyPath:@"owner.age" error:&error];
    if (!result) {
        NSLog(@"%@",error);
    }