KVC的深层讲解

1. KVC基本了解

KVC相信大家再熟悉不过了,键值编码,可以解决很多问题,包括视图上的给UITextField占位文字颜色大小进行设置,获取系统视图进行其颜色样式的自定义,模型转换等等,很多地方可以用KVC,接下来我们就深度解析总结一下KVC。 在iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定,这也是iOS开发中的黑魔法之一。

API

/*
    NSKeyValueCoding.h
    Copyright (c) 1994-2017, Apple Inc. All rights reserved.
*/

#import <Foundation/NSArray.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSOrderedSet.h>
#import <Foundation/NSSet.h>
#import <Foundation/NSException.h>

@class NSError, NSString;

NS_ASSUME_NONNULL_BEGIN


FOUNDATION_EXPORT NSExceptionName const NSUndefinedKeyException;

typedef NSString * NSKeyValueOperator NS_STRING_ENUM;


FOUNDATION_EXPORT NSKeyValueOperator const NSAverageKeyValueOperator;
FOUNDATION_EXPORT NSKeyValueOperator const NSCountKeyValueOperator;
FOUNDATION_EXPORT NSKeyValueOperator const NSDistinctUnionOfArraysKeyValueOperator;
FOUNDATION_EXPORT NSKeyValueOperator const NSDistinctUnionOfObjectsKeyValueOperator;
FOUNDATION_EXPORT NSKeyValueOperator const NSDistinctUnionOfSetsKeyValueOperator;
FOUNDATION_EXPORT NSKeyValueOperator const NSMaximumKeyValueOperator;
FOUNDATION_EXPORT NSKeyValueOperator const NSMinimumKeyValueOperator;
FOUNDATION_EXPORT NSKeyValueOperator const NSSumKeyValueOperator;
FOUNDATION_EXPORT NSKeyValueOperator const NSUnionOfArraysKeyValueOperator;
FOUNDATION_EXPORT NSKeyValueOperator const NSUnionOfObjectsKeyValueOperator;
FOUNDATION_EXPORT NSKeyValueOperator const NSUnionOfSetsKeyValueOperator;

@interface NSObject(NSKeyValueCoding)


@property (class, readonly) BOOL accessInstanceVariablesDirectly;


- (nullable id)valueForKey:(NSString *)key;


- (void)setValue:(nullable id)value forKey:(NSString *)key;


- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;


- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));


- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;


- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;


- (nullable id)valueForUndefinedKey:(NSString *)key;


- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;


- (void)setNilValueForKey:(NSString *)key;


- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;


- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

@end

@interface NSArray<ObjectType>(NSKeyValueCoding)


- (id)valueForKey:(NSString *)key;

/* Invoke -setValue:forKey: on each of the receiver's elements.
*/
- (void)setValue:(nullable id)value forKey:(NSString *)key;

@end

@interface NSDictionary<KeyType, ObjectType>(NSKeyValueCoding)

/* Return the result of sending -objectForKey: to the receiver.
*/
- (nullable ObjectType)valueForKey:(NSString *)key;

@end

@interface NSMutableDictionary<KeyType, ObjectType>(NSKeyValueCoding)

/* Send -setObject:forKey: to the receiver, unless the value is nil, in which case send -removeObjectForKey:.
*/
- (void)setValue:(nullable ObjectType)value forKey:(NSString *)key;

@end

@interface NSOrderedSet<ObjectType>(NSKeyValueCoding)


- (id)valueForKey:(NSString *)key API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));

/* Invoke -setValue:forKey: on each of the receiver's members.
*/
- (void)setValue:(nullable id)value forKey:(NSString *)key API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));

@end

@interface NSSet<ObjectType>(NSKeyValueCoding)


- (id)valueForKey:(NSString *)key;

/* Invoke -setValue:forKey: on each of the receiver's members.
*/
- (void)setValue:(nullable id)value forKey:(NSString *)key;

@end

#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE))

@interface NSObject(NSDeprecatedKeyValueCoding)

/* Methods that were deprecated in Mac OS 10.4.
*/
+ (BOOL)useStoredAccessor API_DEPRECATED("Legacy KVC API", macos(10.0,10.4), ios(2.0,2.0), watchos(2.0,2.0), tvos(9.0,9.0));
- (nullable id)storedValueForKey:(NSString *)key API_DEPRECATED("Legacy KVC API", macos(10.0,10.4), ios(2.0,2.0), watchos(2.0,2.0), tvos(9.0,9.0));
- (void)takeStoredValue:(nullable id)value forKey:(NSString *)key API_DEPRECATED("Legacy KVC API", macos(10.0,10.4), ios(2.0,2.0), watchos(2.0,2.0), tvos(9.0,9.0));

/* Methods that were deprecated in Mac OS 10.3. Use the new, more consistently named, methods declared above instead.
*/
- (void)takeValue:(nullable id)value forKey:(NSString *)key API_DEPRECATED("Legacy KVC API", macos(10.0,10.3), ios(2.0,2.0), watchos(2.0,2.0), tvos(9.0,9.0));
- (void)takeValue:(nullable id)value forKeyPath:(NSString *)keyPath API_DEPRECATED("Legacy KVC API", macos(10.0,10.3), ios(2.0,2.0), watchos(2.0,2.0), tvos(9.0,9.0));
- (nullable id)handleQueryWithUnboundKey:(NSString *)key API_DEPRECATED("Legacy KVC API", macos(10.0,10.3), ios(2.0,2.0), watchos(2.0,2.0), tvos(9.0,9.0));
- (void)handleTakeValue:(nullable id)value forUnboundKey:(NSString *)key API_DEPRECATED("Legacy KVC API", macos(10.0,10.3), ios(2.0,2.0), watchos(2.0,2.0), tvos(9.0,9.0));
- (void)unableToSetNilForKey:(NSString *)key API_DEPRECATED("Legacy KVC API", macos(10.0,10.3), ios(2.0,2.0), watchos(2.0,2.0), tvos(9.0,9.0));
- (NSDictionary *)valuesForKeys:(NSArray *)keys API_DEPRECATED("Legacy KVC API", macos(10.0,10.3), ios(2.0,2.0), watchos(2.0,2.0), tvos(9.0,9.0));
- (void)takeValuesFromDictionary:(NSDictionary *)properties API_DEPRECATED("Legacy KVC API", macos(10.0,10.3), ios(2.0,2.0), watchos(2.0,2.0), tvos(9.0,9.0));

@end

API中包括了:

NSObject的分类NSKeyValueCoding
NSArray的分类NSKeyValueCoding
NSDictionary的分类NSKeyValueCoding
NSMutableDictionary的分类NSKeyValueCoding
NSOrderedSet的分类NSKeyValueCoding
NSSet的分类NSKeyValueCoding
NSObject的一个废弃的分类NSDeprecatedKeyValueCoding

由上我们可以看见,由于NSObject的分类有了这个功能接口,也就是说任意一个对象或者类都有KVC这个功能接口。在API文档中,最重要的就是下面这几个方法。

//直接通过Key来取值
- (nullable id)valueForKey:(NSString *)key;       

//通过Key来设值              
- (void)setValue:(nullable id)value forKey:(NSString *)key;  

//通过KeyPath来取值        
- (nullable id)valueForKeyPath:(NSString *)keyPath;         

//通过KeyPath来设值         
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

例子

1.为属性赋值

#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, copy) NSString *name;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self simpleDemo];
}
#pragma mark - Object Private Function
- (void)simpleDemo{
    [self setValue:@"小明" forKey:@"name"];
    NSLog(@"self.name = %@", self.name);
}
打印数据:
2018-05-16 13:37:50.054908+0700 KVC[20981:421210] self.name = 小明

2. 为成员变量赋值

#import "ViewController.h"


@interface ViewController ()

@end

@implementation ViewController
{
    NSString *location;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self simpleDemo];
}
#pragma mark - Object Private Function
- (void)simpleDemo
{
    [self setValue:@"Beijing" forKey:@"location"];
    NSLog(@"location = %@", location);
}

打印数据:
2018-05-16 13:42:30.878520+0700 KVC[21059:424597] location = Beijing

继承NSObject 就可以使用KVC

如图所示
请添加图片描述

2. 不可不知的赋值深层次原理

  1. KVC可以通过 key直接访问对象的属性
  2. 给对象属性赋值
  3. 运行时动态的访问或修改对象的属性

底层执行机制如下

以[self setValue:@“小明” forKey:@“name”];这句代码作为例子进行说明。

1.程序优先调用setKey:属性方法,代码通过setter方法完成设置。注意这里的Key是指成员变量名,首字母大小写要符合KVC命名规范

2.如果没有找到setName:方法KVC机制会检查+(BOOL)accessInstanceVariablesDirectly方法有没有返回YES如果你重写了该方法让其返回NO的话,那么在这一步KVC会执行setValue:forUndefinedKey:方法不过一般开发者不会这么做。所以KVC机制会搜索该类里面有没有名为<key的成员变量。无论该变量是在类接口处定义,还是在类实现处定义,也无论用了什么样的访问修饰符,只在存在以key>命名的变量,KVC都可以对该成员变量赋值。

3.如果该类即没有set:方法,也没有_成员变量,KVC机制会搜索_is<Key的成员变量。

4.和上面一样,如果该类即没有set:方法,也没有**_和_is成员变量,KVC机制再会继续搜索和is**的成员变量。再给它们赋值。

  1. 如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的setValue:forUndefinedKey:方法,默认是抛出异常。

6.特别需要注意的是:如果开发者想让这个类禁用KVC里,那么重写+ (BOOL)accessInstanceVariablesDirectly方法让其返回NO即可,这样的话如果KVC没有找到set:属性名时,会直接用setValue:forUndefinedKey:方法。

结合例子说明

  • (BOOL)accessInstanceVariablesDirectly 返回NO实例
    实例
#import "ViewController.h"
@interface ViewController ()
@end

@implementation ViewController
{
    NSString *name;
}
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self setValue:@"小明" forKey:@"name"];
    NSLog(@"name = %@", name);
}
+ (BOOL)accessInstanceVariablesDirectly
{
    return NO;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
    NSLog(@"没有找到该key对应的属性会抛出异常");
}

打印数据:

2018-05-16 14:45:42.959735+0700 KVC[21922:462925] 没有找到该key对应的属性会抛出异常
2018-05-16 14:45:42.959850+0700 KVC[21922:462925] name = (null)

总结:

这里+ (BOOL)accessInstanceVariablesDirectly返回了NO,也就是说找不到属性的setter方法,那么不会再去找实例变量,所以会输出上面的结果。所以name输出结果就是null。

(BOOL)accessInstanceVariablesDirectly 返回YES实例

accessInstanceVariablesDirectly 默认返回YES。

实例1 self.name

#import "ViewController.h"
@interface ViewController ()
@property (nonatomic,strong)NSString *name;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self setValue:@"小明" forKey:@"name"];
    NSLog(@"self.name = %@", self.name);
}

实例2 name

#import "ViewController.h"
@interface ViewController ()
@end

@implementation ViewController
{
    NSString *name;
}
- (void)viewDidLoad {
    [super viewDidLoad];
    [self setValue:@"小明" forKey:@"name"];
    NSLog(@"name = %@", name);
}

打印数据:


2018-05-16 14:53:45.648667+0700 KVC[22042:467903] name = 小明

实例3 _name

#import "ViewController.h"
@interface ViewController ()

@end

@implementation ViewController
{
    NSString *_name;
}
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self setValue:@"小明" forKey:@"name"];
    NSLog(@"_name = %@", _name);
}

打印数据:

2018-05-16 14:57:03.714227+0700 KVC[22107:470305] _name = 小明

实例4 _isName

#import "ViewController.h"
@interface ViewController ()

@end

@implementation ViewController
{
    NSString *_isName;
}
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self setValue:@"小明" forKey:@"name"];
    NSLog(@"_isName = %@", _isName);
}

打印数据:

2018-05-16 14:58:24.632017+0700 KVC[22153:471747] _isName = 小明

实例5 isName

#import "ViewController.h"
@interface ViewController ()

@end

@implementation ViewController
{
    NSString *isName;
}
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self setValue:@"小明" forKey:@"name"];
    NSLog(@"isName = %@", isName);
}

打印数据:

2018-05-16 15:00:53.388740+0700 KVC[22223:474376] isName = 小明

下面就是我们这个属性和实例变量的调用顺序。

  • self.name
  • _name
  • _isName
  • name
  • isName

通过上面案例分析可知流程是

[self setValue:@“Beijing” forKey:@“location”];

请添加图片描述

3. 不可不知的取值深层次原理

KVC取值

当调用valueForKey:@”name“的代码时,KVC对key的搜索方式不同于setValue:属性值 forKey:@”name“,其搜索方式如下:

1.首先按get<Key, <key, is<Key的顺序方法查找getter方法,找到的话会直接调用。如果是BOOL或者Int等值类型, 会将其包装成一个NSNumber对象。

2.如果上面的getter没有找到,KVC则会查找countOf, objectIn<KeyAtIndex或<KeyAtIndexes格式的方法。如果countOf<Key方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子类),调用这个代理集合的方法,或者说给这个代理集合发送属于NSArray的方法,就会以countOf<Key,objectIn<KeyAtIndex或<KeyAtIndexes这几个方法组合的形式调用。还有一个可选的get<Key:range:方法。所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名。

3.如果上面的方法没有找到,那么会同时查找countOf,enumeratorOf,memberOf格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合,和上面一样,给这个代理集合发NSSet的消息,就会以countOf,enumeratorOf,memberOf组合的形式调用。

4.如果还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),那么和先前的设值一样,会按_,
_is, , is的顺序搜索成员变量名,这里不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱。如果重写了类方法+
(BOOL)accessInstanceVariablesDirectly返回NO的话,那么会直接调用valueForUndefinedKey:

还没有找到的话,调用valueForUndefinedKey:

#import "ViewController.h"
@interface ViewController ()

@property (nonatomic, assign) NSUInteger count;
@property (nonatomic, copy) NSString *arrName;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    NSNumber* num =   [self valueForKey:@"num"];
    NSLog(@"num:%@",num);
    
    id arr = [self valueForKey:@"numbers"];
    NSLog(@"arr:%@",NSStringFromClass([arr class]));
    NSLog(@"0:%@, 1:%@, 2:%@, 3:%@",arr[0],arr[1],arr[2],arr[3]);
    
    [self incrementCount];
    //count加1
    NSLog(@"[arr count]:%lu",(unsigned long)[arr count]);
//
    [self incrementCount];
    //count再加1
    NSLog(@"[arr count]:%lu",(unsigned long)[arr count]);
    
    [self setValue:@"newName" forKey:@"arrName"];
    NSString *name = [self valueForKey:@"arrName"];
    NSLog(@"name:%@",name);
}

#pragma mark - Objetc Private Function
- (void)incrementCount
{
    self.count ++;
}

- (NSUInteger)countOfNumbers
{
    NSLog(@"countOfNumbers");
    return self.count;
}

//当key使用numbers时,KVC会找到这两个方法。

- (id)objectInNumbersAtIndex:(NSUInteger)index
{
    NSLog(@"+++++objectInNumbersAtIndex");
    return @(index * 2);
}

- (NSInteger)getNum
{
    NSLog(@"getNum---->1");
    return 10;
}

- (NSInteger)num
{
    NSLog(@"num---->2");
    return 11;
}

- (NSInteger)isNum
{
    NSLog(@"isNum---->3");
    return 12;
}

打印数据:
2018-05-17 09:50:46.200687+0700 KVC[26295:638696] getNum---->1
2018-05-17 09:50:46.200798+0700 KVC[26295:638696] num:10
2018-05-17 09:50:46.200947+0700 KVC[26295:638696] arr:NSKeyValueArray
2018-05-17 09:50:46.201045+0700 KVC[26295:638696] +++++objectInNumbersAtIndex
2018-05-17 09:50:46.201135+0700 KVC[26295:638696] 
....
2018-05-17 09:50:46.201506+0700 KVC[26295:638696] countOfNumbers
2018-05-17 09:50:46.201594+0700 KVC[26295:638696] [arr count]:1
2018-05-17 09:50:46.201659+0700 KVC[26295:638696] countOfNumbers
2018-05-17 09:50:46.201723+0700 KVC[26295:638696] [arr count]:2
2018-05-17 09:50:46.201825+0700 KVC[26295:638696] name:newName

通过上面案例分析可知流程是

请添加图片描述

4.keyPath的深度解析

什么时候用keyPath?

然而在开发过程中,一个类的成员变量有可能是自定义类或其他的复杂数据类型,你可以先用KVC获取该属性,然后再次用KVC来获取这个自定义类的属性,但这样是比较繁琐的,对此,KVC提供了一个解决方案,那就是键路径keyPath。

1."Dog.h"
2.
#import <Foundation/Foundation.h>

@interface Dog : NSObject

@property (nonatomic, copy) NSString *name;

@end
3.
#import "ViewController.h"
#import "Dog.h"
@interface ViewController ()

@property (nonatomic,strong)Dog *dog;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self demoKeyPath];
  
}

- (void)demoKeyPath
{
    Dog *dog = [[Dog alloc] init];
    dog.name = @"小黄";
    
    self.dog = dog;
    
    NSString *nameStr1 = self.dog.name;
    NSString *nameStr2 = [self valueForKeyPath:@"dog.name"];
    
    NSLog(@"name = %@", nameStr1);
    NSLog(@"name = %@", nameStr2);
    
    //重新赋值并读取
    [self setValue:@"小花" forKeyPath:@"dog.name"];
    nameStr1 = self.dog.name;
    nameStr2 = [self valueForKeyPath:@"dog.name"];
    
    NSLog(@"name = %@", nameStr1);
    NSLog(@"name = %@", nameStr2);
}

打印数据:
2018-05-17 10:13:28.428006+0700 KVC[26669:654197] name = 小黄
2018-05-17 10:13:28.428147+0700 KVC[26669:654197] name = 小黄
2018-05-17 10:13:28.428249+0700 KVC[26669:654197] name = 小花
2018-05-17 10:13:28.428339+0700 KVC[26669:654197] name = 小花

总结:

大家可以看到,这里属性是另外的一个类,当我们给这个属性自定义类中的属性进行读取值的时候,我们就可以用keyPath,由上看输出,可以看见,可以实现正常的输出。

如果我们不用keyPath,只用key试一下。

#import "ViewController.h"
#import "Dog.h"
@interface ViewController ()

@property (nonatomic,strong)Dog *dog;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self demoKey];
  
}

- (void)demoKey
{
    Dog *dog = [[Dog alloc] init];
    dog.name = @"小黄";
    
    self.dog = dog;
    
    NSString *nameStr1 = self.dog.name;
    NSString *nameStr2 = [self valueForKey:@"dog.name"];
    
    NSLog(@"name = %@", nameStr1);
    NSLog(@"name = %@", nameStr2);
    
    //重新赋值并读取
    [self setValue:@"小花" forKey:@"dog.name"];
    nameStr1 = self.dog.name;
    nameStr2 = [self valueForKey:@"dog.name"];
    
    NSLog(@"name = %@", nameStr1);
    NSLog(@"name = %@", nameStr2);
}

打印数据:
2018-05-17 10:23:42.981124+0700 KVC[26851:661251] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<ViewController 0x7fe5de60fbc0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key dog.name.'
*** First throw call stack:
(
    0   CoreFoundation                      0x000000010fe7d1e6 __exceptionPreprocess + 294
    1   libobjc.A.dylib                     0x000000010f512031 objc_exception_throw + 48
    2   CoreFoundation                      0x000000010fe7d0b9 -[NSException raise] + 9
    3   Foundation                          0x000000010effff58 -[NSObject(NSKeyValueCoding) valueForUndefinedKey:] + 226
    4   Foundation                          0x000000010ef2bebc -[NSObject(NSKeyValueCoding) valueForKey:] + 284
    5   KVC                                 0x000000010ec1037b -[ViewController demoKeyPath] + 235
    6   KVC                                 0x000000010ec10289 -[ViewController viewDidLoad] + 73
    7   UIKit                               0x000000011049e191 -[UIViewController 
                              
  .....
   
    39  KVC                                 0x000000010ec1061f main + 111
    40  libdyld.dylib                       0x00000001138d6955 start + 1
    41  ???                                 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb) 

总结:

大家看这个出错信息,可以看见,因为使用的是key,就会把nameObj.name整个当成key去寻找,很明显这个类里面是找不到这个属性或者变量的,因此会再调用undefinedKey相关方法并抛出异常。而KVC对于keyPath是搜索机制第一步就是分离key,用小数点.来分割key,然后再像普通key一样按照先前介绍的顺序搜索下去。

所以,当我们的属性或者实例变量是基本的系统类型就可以用key进行赋值和取值,但是属性或者实例变量也是另外一个类的时候,想要对该类的属性进行赋值和取值,就要用kayPath。

  • 常用的可以通过setValue:forKey: 和 valueForKey:
  • 也可以通过路由的方式setValue:forKeyPath: 和 valueForKeyPath:

5. KVC几种典型的异常处理

异常场景一:

#import "ViewController.h"
@interface ViewController ()

@property (nonatomic,strong)NSString *name;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self demoExceptionOne];
  
}
//异常场景一:找不到对应的Key
- (void)demoExceptionOne{
     [self setValue:nil forKey:@"person"];
    
}
打印数据:
2018-05-17 13:47:03.307729+0700 KVC[29549:770093] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<ViewController 0x7fa9d0d0cbf0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key person.'
*** First throw call stack:
(
    0   CoreFoundation                      0x00000001034cb1e6 __exceptionPreprocess + 294
 .........
  38  UIKit                               0x00000001039480b7 UIApplicationMain + 159
    39  KVC                                 0x000000010225e36f main + 111
    40  libdyld.dylib                       0x0000000106f24955 start + 1
    41  ???                                 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb) 

解决方案:

重写setValue方法

- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
    NSLog(@"找不到key的异常可以在这里处理");
}
#import "ViewController.h"
@interface ViewController ()

@property (nonatomic,strong)NSString *name;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self demoExceptionOne];
  
}
//异常场景一:找不到对应的Key
- (void)demoExceptionOne{
     [self setValue:nil forKey:@"person"];
    
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
    NSLog(@"找不到key的异常可以在这里处理");
}

打印数据:
2018-05-17 13:49:59.475353+0700 KVC[29620:772526] 找不到key的异常可以在这里处理

异常场景二:

#import "ViewController.h"
@interface ViewController ()

@property (nonatomic,strong)NSString *name;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self demoExceptionTwo];
  
}

//异常场景2:传值传nil
- (void)demoExceptionTwo{
    [self setValue:nil forKey:@"name"];
    NSLog(@"name = %@", self.name);
}
打印数据:
2018-05-17 13:53:54.789612+0700 KVC[29687:775199] name = (null)

可见,你value即使传入的是nil,但是程序也不会崩溃,他会自动的给这个属性赋值为null。这里之所以不崩溃是因为我定义的属性name属于对象,对象可以有nil类型,但是如果我定义的属性是非对象的呢,下面看一下验证代码。

#import "ViewController.h"
@interface ViewController ()

@property (nonatomic,assign)int age;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self demoExceptionTwo];
  
}

//异常场景2:传值传nil
- (void)demoExceptionTwo{
    [self setValue:nil forKey:@"age"];
    NSLog(@"age = %d", self.age);
}
打印数据:
2018-05-17 13:59:02.105192+0700 KVC[29775:778629] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '[<ViewController 0x7fd37760ffd0> setNilValueForKey]: could not set nil as the value for the key age.'
*** First throw call stack:
(
    0   CoreFoundation                      0x0000000105c881e6 __exceptionPreprocess + 294
    3   Foundation                          0x0000000104e0b0af -[NSObject(NSKeyValueCoding) setNilValueForKey:] + 81
........
    40  KVC                                 0x0000000104a1b37f main + 111
    41  libdyld.dylib                       0x00000001096e1955 start + 1
    42  ???                                 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb) 

总结
这里age我定义的属性不是对象,而是一个整型,下面我们运行你就会发现这次不能运行了,崩溃并抛出异常了
,因为name属性是对象,所以赋值为nil不会崩溃,对象类型可以为nil;但是age是整数,整数的类型不会是nil,这么强行赋值就会抛出异常出现错误。如果你不小心传了nil,KVC会调用setNilValueForKey:方法。这个方法默认是抛出异常。

解决方法:
重写setNilValueForKey

#import "ViewController.h"
@interface ViewController ()

@property (nonatomic,assign)int age;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self demoExceptionTwo];
  
}

//异常场景2:传值传nil
- (void)demoExceptionTwo{
    [self setValue:nil forKey:@"age"];
    NSLog(@"age = %d", self.age);
}

- (void)setNilValueForKey:(NSString *)key
{
    NSLog(@"属性值不能为nil");
}
打印数据:
2018-05-17 14:05:50.040561+0700 KVC[29895:783504] 属性值不能为nil
2018-05-17 14:05:50.040696+0700 KVC[29895:783504] age = 0

6.KVC容器类及深层次原理

容器类

容器类主要是指NSArray、NSMutableArray、NSDictionary、NSMutableDictionary等有序容器和NSSet等无序容器。

有序不可变容器类

对于NSArray可以通过valueForKey:进行访问,如下代码所示

#import "ViewController.h"
@interface ViewController ()

@property (nonatomic, strong) NSArray *kvcArr;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self demoArray];
}

- (void)demoArray
{
    self.kvcArr = @[@1, @2, @3, @4];
    
    NSObject *obj = [self valueForKey:@"kvcArr"];
    
    NSLog(@"obj = %@", obj);
}

打印数据:
2018-05-17 14:46:38.862732+0700 KVC[30470:807433] obj = (
    1,
    2,
    3,
    4
)

对于NSArray可以通过setValue: forKey:进行修改数组,如下代码所示

- (void)viewDidLoad {
    [super viewDidLoad];
  
    
    [self changeArray];
}

- (void)changeArray
{
    self.kvcArr = @[@1, @2, @3, @4];
    [self setValue:@[@"100",@"200",@"300",@"400"] forKey:@"kvcArr"];
    NSObject *obj = [self valueForKey:@"kvcArr"];
    
    NSLog(@"obj = %@", obj);
}
打印数据
obj = (
    100,
    200,
    300,
    400
)

或者NSMutableArray

- (void)changeArray
{
    self.kvcArr = @[@1, @2, @3, @4];
    NSMutableArray *mArray = [self mutableArrayValueForKey:@"kvcArr"];
    mArray[0] = @200;
    NSObject *obj = [self valueForKey:@"kvcArr"];
    NSLog(@"obj = %@", obj);
}

打印结果
obj = (
    200,
    2,
    3,
    4
)

对于NSDictionary也是一样的,下面看一下代码。

#import "ViewController.h"
@interface ViewController ()

@property (nonatomic, strong) NSDictionary *kvcDict;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self demoDictionary];
}

- (void)demoDictionary{
    self.kvcDict = @{@"1" : @"One", @"2" : @"Two", @"3" : @"Three"};
    
    NSObject *obj = [self valueForKey:@"kvcDict"];
    
    NSLog(@"obj = %@", obj);
}

打印数据:
2018-05-17 14:49:18.219101+0700 KVC[30534:809791] obj = {
    1 = One;
    2 = Two;
    3 = Three;
}
从上面我们可以看出对于有序不可变容器类NSArray和NSDictionary等等,都可以使用valueForKey :这个方法来获取该属性。

有序可变容器类

下面我们看一下可变容器类,比如说NSMutableArray和NSMutableDictionary。比如对于NSMutableArray可以使用下面方法获取数组。

#import "ViewController.h"
@interface ViewController ()

@property (nonatomic, strong) NSMutableArray *kvcArrM;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self demoNSMutableArray];
}

- (void)demoNSMutableArray
{
    NSArray *arr = @[@1, @2, @3, @4];
    self.kvcArrM = [NSMutableArray arrayWithArray:arr];
    
    NSObject *obj = [self mutableArrayValueForKey:@"kvcArrM"];
    
    NSLog(@"obj = %@", obj);
}

打印数据:
2018-05-17 15:37:17.358111+0700 KVC[31200:836978] obj = (
    1,
    2,
    3,
    4
)

下面我们看一下方法- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;的深层次原理。

搜索insertObject:inAtIndex: , removeObjectFromAtIndex:或者 insertAdIndexes ,removeAtIndexes格式的方法。如果至少找到一个insert方法和一个remove方法,那么同样返回一个可以响应NSMutableArray所有方法代理集合(类名是NSKeyValueFastMutableArray2),那么给这个代理集合发送NSMutableArray的方法,以insertObject:inAtIndex:, removeObjectFromAtIndex: 或者 insertAdIndexes , removeAtIndexes组合的形式调用。还有两个可选实现的接口:replaceOnjectAtIndex:withObject:,replaceAtIndexes:with:。

如果上步的方法没有找到,则搜索set: 格式的方法,如果找到,那么发送给代理集合的NSMutableArray最终都会调用set:方法。 也就是说,mutableArrayValueForKey:取出的代理集合修改后,用set:重新赋值回去去。这样做效率会低很多。所以推荐实现上面的方法。

如果上一步的方法还还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),会按_、,的顺序搜索成员变量名,如果找到,那么发送的NSMutableArray消息方法直接交给这个成员变量处理。

如果还是找不到,则调用valueForUndefinedKey:。

关于mutableArrayValueForKey:的适用场景

发现其一般是用在对NSMutableArray添加Observer上。如果对象属性是个NSMutableArray、NSMutableSet、NSMutableDictionary等集合类型时,你给它添加KVO时,你会发现当你添加或者移除元素时并不能接收到变化

因为KVO的本质是系统监测到某个属性的内存地址或常量改变时,会添加上- (void)willChangeValueForKey:(NSString *)key和- (void)didChangeValueForKey:(NSString *)key方法来发送通知,

所以一种解决方法

1.是手动调用者两个方法,但是并不推荐,你永远无法像系统一样真正知道这个元素什么时候被改变。

2.另一种便是利用使用mutableArrayValueForKey:了。

#import "ZFSecondVC.h"

@interface ZFSecondVC ()
@property (nonatomic, strong) NSMutableArray *kvcArrM;

@end

@implementation ZFSecondVC

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
}

- (void)dealloc
{
    [self removeObserver:self forKeyPath:@"kvcArrM"];
}

#pragma mark - Object Private Function

- (void)demoMutableArrayValueForKey
{
    self.kvcArrM = [NSMutableArray array];
    [self addObserver:self forKeyPath:@"kvcArrM" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}

- (void)addItem
{
    [self.kvcArrM addObject:@"One"];
}

- (void)addAntherItem
{
    [[self mutableArrayValueForKey:@"kvcArrM"] addObject:@"Two"];
    [[self mutableArrayValueForKey:@"kvcArrM"] addObject:@"Three"];
}

- (void)removeLastItem
{
    [[self mutableArrayValueForKey:@"kvcArrM"] removeLastObject];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"change = %@", change);
    NSLog(@"arrM = %@", self.kvcArrM);
}

#import "ViewController.h"
#import "ZFSecondVC.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    ZFSecondVC *VC = [[ZFSecondVC alloc] init];
    
    [VC demoMutableArrayValueForKey];
    [VC addItem];
    [VC addAntherItem];
    [VC removeLastItem];
}
2018-05-17 16:03:54.130555+0700 KVC[31726:857467] change = {
    indexes = "<_NSCachedIndexSet: 0x600000222520>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
    kind = 2;
    new =     (
        Two
    );
}
2018-05-17 16:03:54.130703+0700 KVC[31726:857467] arrM = (
    One,
    Two
)
2018-05-17 16:03:54.130910+0700 KVC[31726:857467] change = {
    indexes = "<_NSCachedIndexSet: 0x600000222820>[number of indexes: 1 (in 1 ranges), indexes: (2)]";
    kind = 2;
    new =     (
        Three
    );
}
2018-05-17 16:03:54.131154+0700 KVC[31726:857467] arrM = (
    One,
    Two,
    Three
)
2018-05-17 16:03:54.131295+0700 KVC[31726:857467] change = {
    indexes = "<_NSCachedIndexSet: 0x600000222820>[number of indexes: 1 (in 1 ranges), indexes: (2)]";
    kind = 3;
    old =     (
        Three
    );
}
2018-05-17 16:03:54.131417+0700 KVC[31726:857467] arrM = (
    One,
    Two
)

可变数组里面元素的改变可以被KVO监听到了。当调用[self.kvcArrM addObject:@“One”];时不会触发KVO,只有调用方式改变为[[self mutableArrayValueForKey:@“kvcArrM”] addObject:@“Two”];才会触发KVO。

无序可变容器类

对于无序可变容器常用的就是NSMutableSet,对于可变无序容器类和上面讲的可变有序容器类的数组有点相似,主要是下面这个方法。

  • (NSMutableSet *)mutableSetValueForKey:(NSString *)key;

下面看一下调用该方法底层深层次的一些东西。

搜索addObjectObject: , removeObject: 或者 add , remove格式的方法
如果至少找到一个insert方法和一个remove方法,那么同样返回一个可以响应NSMutableSet所有方法代理集合(类名是NSKeyValueFastMutableSet2),那么给这个代理集合发送NSMutableSet的方法,以addObjectObject: , removeObject: 或者 add , remove组合的形式调用。还有两个可选实现的接口:intersect , set:

如果receiver是ManagedObject,那么就不会继续搜索。

如果上一步的方法没有找到,则搜索set: 格式的方法,如果找到,那么发送给代理集合的NSMutableSet最终都会调用set:方法。 也就是说,mutableSetValueForKey取出的代理集合修改后,用set: 重新赋值回去去。这样做效率会低很多。所以推荐实现上面的方法。

如果上一步的方法还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),会按_, 的顺序搜索成员变量名,如果找到,那么发送的NSMutableSet消息方法直接交给这个成员变量处理。

如果还是找不到,调用valueForUndefinedKey:
可见,除了检查receiver是ManagedObject以外,其搜索顺序和mutableArrayValueForKey基本一至。

7. KVC正确性的验证

当开发者需要验证能不能用KVC设定某个值时,就需要在进行KVC赋值前验证值value的有效性,API文档里面提供下面的方法进行判断值的有效性。

- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

该方法的工作原理:先找一下你的类中是否实现了方法-(BOOL)validate:error:,如果实现了就会根据实现方法里面的自定义逻辑返回NO或者YES,如果没有实现这个方法,则系统默认返回就是YES。

#import "ViewController.h"
@interface ViewController ()

@property (nonatomic, copy) NSString *personName;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor whiteColor];
    
    NSError *error;
    NSString *key = @"personName";
    NSString *value = @"小明";
    BOOL result = [self validateValue:&value forKey:key error:&error];
    
    if (error) {
        NSLog(@"error = %@", error);
        return;
    }
    
    if (result) {
        NSLog(@"验证正确是小明");
    }
    else {
        NSLog(@"不是小明");
    }
}

- (BOOL)validatePersonName:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError
{
    NSString *name = *value;
    if ([name isEqualToString:@"小明"]) {
        return YES;
    }
    return NO;
}

打印数据:
2018-05-17 16:38:32.998141+0700 KVC[32204:879065] 验证正确是小明

这里首先调用方法[self validateValue:&value forKey:key error:&error];,这里,由于我实现了方法- (BOOL)validatePersonName:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError,所以就在这里进行值value有效性的判断,这里[name isEqualToString:@“小明”]我就给返回YES,否则就返回NO。

上面这个小的示例还是很简单的,就是验证value赋值前的有效性判断。

8.KVC几种常见应用

取值和赋值

KVC这种基于运行时的编程方式极大地提高了灵活性,简化了代码,甚至实现很多难以想像的功能,KVC也是许多iOS开发黑魔法的基础。在取值和赋值方面的应用,前面几篇讲解别的知识点的时候已经说了很多了,这里就不给大家实例代码了,大家要记住属性和成员变量的调用顺序以及赋值取值的深层次原理。

获取集合对象

这里需要注意的是,取值和赋值不仅可以指单个的属性或者变量值,还可以操作集合对象,比如NSArray或NSSet等,用的方法也就是valueForKey:和setValueForKey:。

访问和修改私有变量

大家都知道,一个类,只有在.h文件中暴露的属性,别的类才可以引入.h文件的时候并获取其中的属性值,但是如果是私有属性,那么在别的类里面你就不能修改私有属性的值,这个时候KVC就可以了。可以通过KVC为私有属性进行赋值。不多说。

//设置状态栏颜色
- (void)setStatusBarBackgroundColor:(UIColor *)color {
    UIView *statusBar = [[[UIApplication sharedApplication] valueForKey:@"statusBarWindow"] valueForKey:@"statusBar"];
    if ([statusBar respondsToSelector:@selector(setBackgroundColor:)]) {
//        statusBar.backgroundColor = color;
        [statusBar setValue:color forKey:@"backgroundColor"];
    }
   }

实现多层的消息传递

这里说的多层消息传递,主要指的就是对容器类集合元素应用KVC,对集合元素进行操作时,不是对容器本身进行操作,而是对容器中的每个元素进行操作,返回的是结果也是一个集合元素。

#import "ViewController.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    NSArray *arr = @[@"john", @"tom", @"lucy", @"lily"];
    NSArray *captainArr = [arr valueForKey:@"capitalizedString"];
    NSLog(@"captainArr = %@", captainArr);
    
    NSArray *lengthArr = [arr valueForKeyPath:@"capitalizedString.length"];
    NSLog(@"lengthArr = %@",lengthArr);
}

打印数据:
2018-05-17 17:19:38.889615+0700 KVC[33165:914790] captainArr = (
    John,
    Tom,
    Lucy,
    Lily
)
2018-05-17 17:19:38.889844+0700 KVC[33165:914790] lengthArr = (
    4,
    3,
    4,
    4
)

总结:
从上面代码可以看出来,[arr valueForKey:@“capitalizedString”]方法就是对arr数组的每一个元素进行操作去首字母为大写;[arr valueForKeyPath:@“capitalizedString.length”]方法不仅对每一个元素取大写开头,而且还求长度。

KVC中的函数对集合进行操作

#import "ViewController.h"
#import "Book.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    Book *book1 = [Book new];
    book1.name = @"The Great Gastby";
    book1.price = 22;
    
    Book *book2 = [Book new];
    book2.name = @"Time History";
    book2.price = 12;
    
    Book *book3 = [Book new];
    book3.name = @"Wrong Hole";
    book3.price = 111;
    
    Book *book4 = [Book new];
    book4.name = @"Wrong Hole";
    book4.price = 111;
    
    NSArray* arrBooks = @[book1,book2,book3,book4];
    NSNumber* sum = [arrBooks valueForKeyPath:@"@sum.price"];
    NSLog(@"总价:sum:%f",sum.floatValue);
    
    NSNumber* avg = [arrBooks valueForKeyPath:@"@avg.price"];
    NSLog(@"均价:avg:%f",avg.floatValue);
    
    NSNumber* count = [arrBooks valueForKeyPath:@"@count"];
    NSLog(@"个数:count:%f",count.floatValue);
    
    NSNumber* min = [arrBooks valueForKeyPath:@"@min.price"];
    NSLog(@"最小值:min:%f",min.floatValue);
    
    NSNumber* max = [arrBooks valueForKeyPath:@"@max.price"];
    NSLog(@"最大值:max:%f",max.floatValue);
}

打印数据:
2018-05-17 17:32:38.456676+0700 KVC[33432:924972] 总价:sum:256.000000
2018-05-17 17:32:38.456855+0700 KVC[33432:924972] 均价:avg:64.000000
2018-05-17 17:32:38.456954+0700 KVC[33432:924972] 个数:count:4.000000
2018-05-17 17:32:38.457048+0700 KVC[33432:924972] 最小值:min:12.000000
2018-05-17 17:32:38.457180+0700 KVC[33432:924972] 最大值:max:111.000000


  • 对象运算符

比集合运算符稍微复杂,能以数组的方式返回指定的内容,一共有两种:

@distinctUnionOfObjects
@unionOfObjects

它们的返回值都是NSArray,区别是前者返回的元素都是唯一的,是去重以后的结果;后者返回的元素是全集。

#import "ViewController.h"
#import "Book.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    Book *book1 = [Book new];
    book1.name = @"The Great Gastby";
    book1.price = 22;
    
    Book *book2 = [Book new];
    book2.name = @"Time History";
    book2.price = 12;
    
    Book *book3 = [Book new];
    book3.name = @"Wrong Hole";
    book3.price = 111;
    
    Book *book4 = [Book new];
    book4.name = @"Wrong Hole";
    book4.price = 111;
    
    NSArray* arrBooks = @[book1,book2,book3,book4];
    NSLog(@"distinctUnionOfObjects");
    NSArray* arrDistinct = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
    for (NSNumber *price in arrDistinct) {
        NSLog(@"%f",price.floatValue);
    }
    
    NSLog(@"unionOfObjects");
    NSArray* arrUnion = [arrBooks valueForKeyPath:@"@unionOfObjects.price"];
    for (NSNumber *price in arrUnion) {
        NSLog(@"%f",price.floatValue);
    }
}

打印数据:
2018-05-18 10:06:57.510192+0700 KVC[35322:1002012] distinctUnionOfObjects
2018-05-18 10:06:57.510397+0700 KVC[35322:1002012] 111.000000
2018-05-18 10:06:57.510499+0700 KVC[35322:1002012] 12.000000
2018-05-18 10:06:57.510753+0700 KVC[35322:1002012] 22.000000
2018-05-18 10:06:57.510842+0700 KVC[35322:1002012] unionOfObjects
2018-05-18 10:06:57.510939+0700 KVC[35322:1002012] 22.000000
2018-05-18 10:06:57.511106+0700 KVC[35322:1002012] 12.000000
2018-05-18 10:06:57.511185+0700 KVC[35322:1002012] 111.000000
2018-05-18 10:06:57.511407+0700 KVC[35322:1002012] 111.000000

  • Array和Set操作符
@distinctUnionOfArrays
@unionOfArrays
@distinctUnionOfSets

(1) @distinctUnionOfArrays:该操作会返回一个数组,这个数组包含不同的对象,不同的对象是在从关键路径到操作器右边的被指定的属性里。
(2) @unionOfArrays 该操作会返回一个数组,这个数组包含的对象是在从关键路径到操作器右边的被指定的属性里和@distinctUnionOfArrays不一样,重复的对象不会被移除。
(3) @distinctUnionOfSets和@distinctUnionOfArrays类似。因为Set本身就不支持重复。

9. KVC模型转化(1) 模型打印 description, debugDescription

直接上代码

1.Person.h

#import <Foundation/Foundation.h>

@interface Person : NSObject

@property (nonatomic,strong)NSString *name;
@property (nonatomic,assign)int age;


@end
2.Person.m

#import "Person.h"
#import <objc/runtime.h>
@implementation Person

- (NSString *)description{
    NSMutableDictionary *dic = [NSMutableDictionary dictionary];
    //得到当前class的所有属性
    uint count;
    objc_property_t *properties = class_copyPropertyList([self class], &count);
    for (int i = 0; i<count; i++) {
        objc_property_t property = properties[I];
        NSString *name = @(property_getName(property));
        id value = [self valueForKey:name]?:[NSNull null];//此处注意不能为nil 字典中尽量不要出现nil
        [dic setObject:value forKey:name];
    }
    free(properties);
    return [NSString stringWithFormat:@"description:<%@:%p> -- %@",[self class],self,dic];
}

- (NSString *)debugDescription{
    NSMutableDictionary *dic = [NSMutableDictionary dictionary];
    //得到当前class的所有属性
    uint count;
    objc_property_t *properties = class_copyPropertyList([self class], &count);
    for (int i = 0; i<count; i++) {
        objc_property_t property = properties[I];
        NSString *name = @(property_getName(property));
        id value = [self valueForKey:name]?:[NSNull null];//此处注意不能为nil 字典中尽量不要出现nil
        [dic setObject:value forKey:name];
    }
    free(properties);
    return [NSString stringWithFormat:@"debugDescription:<%@:%p> -- %@",[self class],self,dic];
}

@end
3.ViewController.m
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    Person *p = [[Person alloc]init];
    p.name = @"小明";
    p.age = 10;
    NSLog(@"p:%@",p);
}

打印

2018-05-18 17:54:09.925353+0700 KVC[44115:1324816] p:description:<Person:0x604000031fe0> -- {
    age = 10;
    name = "\U5c0f\U660e";
}

总结:当我们对p进行打印时会直接触发“description”方法,那么“debugDescription”在什么时间调用,请看下图

在这里插入图片描述

总结:
由上图我们可以看到,当对p进行打印的时候,直接触发调用了“description”,而我们对p进行控制台打印的时候 会触发“debugDescription”函数,已经很明白了 ,不需要进行解释了。

10.模型转换

传统设置方法

#import <Foundation/Foundation.h>

@interface Person : NSObject

@property (nonatomic,strong)NSString *name;
@property (nonatomic,assign)int age;

+ (instancetype)personWithDict:(NSDictionary *)dict;
- (instancetype)initWithDict:(NSDictionary *)dict;

@end
#import "Person.h"
#import <objc/runtime.h>
@implementation Person

- (NSString *)description{
    NSMutableDictionary *dic = [NSMutableDictionary dictionary];
    //得到当前class的所有属性
    uint count;
    objc_property_t *properties = class_copyPropertyList([self class], &count);
    for (int i = 0; i<count; i++) {
        objc_property_t property = properties[i];
        NSString *name = @(property_getName(property));
        id value = [self valueForKey:name]?:[NSNull null];//默认值不可为为nil的字符串
        [dic setObject:value forKey:name];
    }
    free(properties);
    return [NSString stringWithFormat:@"%@:%@",[self class],dic];
}

+ (instancetype)personWithDict:(NSDictionary *)dict {
    return [[self alloc] initWithDict:dict];
}

- (instancetype)initWithDict:(NSDictionary *)dict {
    self = [super init];
    if (self) {
        // 方法 1,直接设置,基本数据类型需要转换
        _name = dict[@"name"];
        _age = [dict[@"age"] intValue];
    }
    return self;
}

#import "ViewController.h"

#import "Person.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    NSDictionary *dic = @{
                          @"name":@"小明",
                          @"age":@10,
                          @"address":@"河南"
                          };
    Person *p = [Person personWithDict:dic];
    NSLog(@"%@",p);
}


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

打印数据

018-05-18 20:50:38.774764+0700 KVCDemo[4798:382355] Person:{
    age = 10;
    name = "\U5c0f\U660e";
}

无论字典中有没有包含模型的字段,都不会导致崩溃,包含赋值,不包含不赋值,多不退,少不补。

KVC基础设置方法

#import <Foundation/Foundation.h>

@interface Person : NSObject

@property (nonatomic,copy)NSString * _Nullable name;
@property (nonatomic,assign)int age;
@property (nonnull,copy)NSString *adress;

+ (instancetype _Nullable )personWithDict:(NSDictionary *_Nullable)dict;
- (instancetype _Nullable )initWithDict:(NSDictionary *_Nullable)dict;

@end
#import "Person.h"
#import <objc/runtime.h>
@implementation Person

- (NSString *)description{
    NSMutableDictionary *dic = [NSMutableDictionary dictionary];
    //得到当前class的所有属性
    uint count;
    objc_property_t *properties = class_copyPropertyList([self class], &count);
    for (int i = 0; i<count; i++) {
        objc_property_t property = properties[i];
        NSString *key = @(property_getName(property));
        id value = [self valueForKey:key]?:[NSNull null];//此处不能为nil 因为字典中尽量不要出现nil 应该为[NSNull null]
        [dic setObject:value forKey:key];
    }
    free(properties);
    return [NSString stringWithFormat:@"%@:%@",[self class],dic];
}



+ (instancetype)personWithDict:(NSDictionary *)dict {
    return [[self alloc] initWithDict:dict];
}

- (instancetype)initWithDict:(NSDictionary *)dict {
    self = [super init];
    if (self) {
        [self setValue:dict[@"name"] forKey:@"name"];
        [self setValue:dict[@"age"] forKey:@"age"];
        [self setValue:dict[@"adress"] forKey:@"adress"];
        
    }
    return self;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
    NSLog(@"不存在Key:%@",key);
}

- (void)setNilValueForKey:(NSString *)key{
    NSLog(@"属性值不能为nil");
}
@end

#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    NSDictionary *dic = @{
                          @"name":[NSNull null],
                          @"adress":@"河南",
                          @"age":@10
                          };
    Person *p = [Person personWithDict:dic];
    NSLog(@"%@",p);
}

打印数据:

2018-05-18 22:38:44.749136+0700 KVCDemo[6297:464723] Person:{
    adress = "\U6cb3\U5357";
    age = 10;
    name = "<null>";
}

5.总结:

1 - (void)setValue:(id)value forUndefinedKey:(NSString *)key
模型中不存在这个key会调用此方法。
2- (void)setNilValueForKey:(NSString *)key
当模型中的整形不存在时调用此方法。 并且如果整形不存在系统会将0赋予这个整型的字段。
3.这两个方法是必须执行的,因为如果不执行,如果存在不存在的key 或者模型的整形字段不存在,都会导致程序的崩溃。

遍历字典

#import <Foundation/Foundation.h>

@interface Person : NSObject

@property (nonatomic,copy)NSString * _Nullable name;
@property (nonatomic,assign)int age;
@property (nonnull,copy)NSString *adress;


+ (instancetype _Nullable )personWithDict:(NSDictionary *_Nullable)dict;
- (instancetype _Nullable )initWithDict:(NSDictionary *_Nullable)dict;

@end

#import "Person.h"
#import <objc/runtime.h>
@implementation Person

- (NSString *)description{
    NSMutableDictionary *dic = [NSMutableDictionary dictionary];
    //得到当前class的所有属性
    uint count;
    objc_property_t *properties = class_copyPropertyList([self class], &count);
    for (int i = 0; i<count; i++) {
        objc_property_t property = properties[i];
        NSString *key = @(property_getName(property));
        id value = [self valueForKey:key]?:[NSNull null];//此处不能为nil 因为字典中尽量不要出现nil 应该为[NSNull null]
        [dic setObject:value forKey:key];
    }
    free(properties);
    return [NSString stringWithFormat:@"%@:%@",[self class],dic];
}



+ (instancetype)personWithDict:(NSDictionary *)dict {
    return [[self alloc] initWithDict:dict];
}

- (instancetype)initWithDict:(NSDictionary *)dict {
    self = [super init];
    if (self) {
        for (NSString *key in dict) {
            id value = dict[key];
            [self setValue:value forKey:key];
        }
        
    }
    return self;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
    NSLog(@"不存在Key:%@",key);
}

- (void)setNilValueForKey:(NSString *)key{
    NSLog(@"属性值不能为nil");
}
- (BOOL)validatePersonName:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError
{
    NSString *name = *value;
    if ([name isEqualToString:@"小明"]) {
        return YES;
    }
    return NO;
}

#import "ViewController.h"
#import "Person.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    NSDictionary *dic = @{
                          @"name":[NSNull null],
                          @"adress":@"河南",
                          @"age":@10
                          };
    Person *p = [Person personWithDict:dic];
    NSLog(@"%@",p);
}

打印数据:

2018-05-19 16:17:55.638697+0700 KVCDemo[7364:534839] Person:{
    adress = "\U6cb3\U5357";
    age = 10;
    name = "<null>";
}

setValuesForKeysWithDictionary

#import <Foundation/Foundation.h>

@interface Person : NSObject

@property (nonatomic,copy)NSString * _Nullable name;
@property (nonatomic,assign)int age;
@property (nonnull,copy)NSString *adress;


+ (instancetype _Nullable )personWithDict:(NSDictionary *_Nullable)dict;
- (instancetype _Nullable )initWithDict:(NSDictionary *_Nullable)dict;

@end
#import "Person.h"
#import <objc/runtime.h>
@implementation Person

- (NSString *)description{
    NSMutableDictionary *dic = [NSMutableDictionary dictionary];
    //得到当前class的所有属性
    uint count;
    objc_property_t *properties = class_copyPropertyList([self class], &count);
    for (int i = 0; i<count; i++) {
        objc_property_t property = properties[i];
        NSString *key = @(property_getName(property));
        id value = [self valueForKey:key]?:[NSNull null];//此处不能为nil 因为字典中尽量不要出现nil 应该为[NSNull null]
        [dic setObject:value forKey:key];
    }
    free(properties);
    return [NSString stringWithFormat:@"%@:%@",[self class],dic];
}



+ (instancetype)personWithDict:(NSDictionary *)dict {
    return [[self alloc] initWithDict:dict];
}

- (instancetype)initWithDict:(NSDictionary *)dict {
    self = [super init];
    if (self) {
        [self setValuesForKeysWithDictionary:dict];
    }
    return self;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
    NSLog(@"不存在Key:%@",key);
}

- (void)setNilValueForKey:(NSString *)key{
    NSLog(@"属性值不能为nil");
}
- (BOOL)validatePersonName:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError
{
    NSString *name = *value;
    if ([name isEqualToString:@"小明"]) {
        return YES;
    }
    return NO;
}

#import "ViewController.h"
#import "Person.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    NSDictionary *dic = @{
                          @"name":[NSNull null],
                          @"adress":@"河南",
                          @"age":@10
                          };
    Person *p = [Person personWithDict:dic];
    NSLog(@"%@",p);
}

打印数据:

2018-05-19 16:29:15.202777+0700 KVCDemo[7492:542825] Person:{
    adress = "\U6cb3\U5357";
    age = 10;
    name = "<null>";
}

此方法等同于遍历字典,设置数据,等同于方法3

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值