美文网首页iOS开发IOS将来跳槽用
Key-Value Coding(键值编码)

Key-Value Coding(键值编码)

作者: 好_快 | 来源:发表于2016-09-01 17:31 被阅读693次

    一、KVC简介

    KVC提供了一套不通过访问器方法或者属性变量,通过Key或者KeyPath直接访问对象属性的机制。KVC是以下技术的实现基础KVO、Core Data、Cocoa bindings、AppleScript。KVC性能略逊于访问器和实例变量,但是灵活性高,很多时候可以简化代码。使用KVC需要实现其存取方法,相关的方法都在Objective-C的NSKeyValueCoding协议中声明,超级父类NSObject默认遵守该协议。KVC支持对象属性(如NSSting)同时也指出非对象属性(基本数据类型和结构体,提供自动转换数据类型)。

    二、KVC基本原理

    首先区分两个基本概念

    名称 内容
    Key Key是标识对象具体属性的字符串,相当于对象的访问器名称或者变量名称,不能包含空格。
    KeyPath KeyPath是指定对象一系列属性,且用.分割每个属性的字符串。字符串序列中的每个key标识前面对象的属性。比如说people.address.street能够获取people的address属性,然后获取到address的street属性。

    然后说明等的执行过程,KVC的方法从功能上分存、取两种方法setValue:forKey:valueForKey:,以这两个方法为代表描述执行过程。

    首先setValue:forKey:的执行过程
    1、首先对象方法列表中匹配方法-set<Key>:

    2、如果第1步失败而且 accessInstanceVariablesDirectly 返回YES,按照以下顺序匹配实例变量_<key>, _is<Key>, <key>, or is<Key>

    3、如果前2步任一成功,则进行赋值。必要的话进行数据类型转换。

    4、如果前3步进行失败则调用 setValue:forUndefinedKey: 抛出NSUndefinedKeyException异常。

    注:方法setValue:forKey:根据指定路径获取属性值,KeyPath中每一个key都进行以上步骤;也就是说任何一个key出错,都会抛出异常。

    代码2.1
    @interface ViewController ()
    {
        NSString *_name;
        NSString *_isName;
        NSString *name;
        NSString *isName;
    
    }
    @property (nonatomic,copy)NSString *name;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        [self setValue:@"zwq" forKey:@"name"];
        
        NSLog(@"_name:%@",_name);
        NSLog(@"_isName:%@",_isName);
        NSLog(@"name:%@",name);
        NSLog(@"isName:%@",isName);
        }
    //可以通过以上代码(注释部分代码)来验证上述过程。    
    

    然后是valueForKey:执行过程

    1、首先按照此顺序匹配方法 get<Key>, <key>, or is<Key>, 如果匹配成功调用方法,返回结果。必要的话进行数据类型转换。

    2、如果1步进行失败,则匹配以下方法 countOf<Key>、 objectIn<Key>AtIndex: 、 <key>AtIndexes:若找打其中一个,则返回容器类对象。该对象调用以上方法,会调用valueForKey:方法。(NSArray类的方法)

    3、如果前2步失败,则匹配以下方法countOf<Key>, enumeratorOf<Key>, and memberOf<Key>:若找打其中一个,则返回容器类对象。该对象调用以上方法,会调用valueForKey:方法。
    (NSSet类的方法)

    4、如果前3步失败,而且 accessInstanceVariablesDirectly 返回YES,按照以下顺序匹配实例变量_<key>, _is<Key>, <key>, or is<Key>。如果实例变量找到了,则进行复制。必要的话进行数据类型转换。

    5、如果前4步进行失败则调用 valueForUndefinedKey: 抛出NSUndefinedKeyException异常。

    注:
    1、方法valueForKeyPath:根据指定路径获取属性值,KeyPath中每一个key都进行以上步骤;也就是说任何一个key出错,都会抛出异常。
    2、如果KeyPath序列中包含了一个key是一对多的关系,而且这个key不是最后一个,那么将返回所有对象的属性值。例如accounts.transactions.payee将返回所有account的所有transaction的所有payee值。

    //VC有一个数组属性
    @property (nonatomic,assign)NSArray *array;
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        //Data有一个name属性
        Data *data1 = [[Data alloc] init];
        Data *data2 = [[Data alloc] init];
        Data *data3 = [[Data alloc] init];
        data1.name=@"data1";
        data2.name=@"data2";
        data3.name=@"data3";
        
        //self.array.name
        NSArray *arr = [NSArray arrayWithObjects:data1,data2,data3, nil];
        [self setValue:arr forKey:@"array"];
        NSLog(@"array:%@",[self valueForKeyPath:@"array.name"]);
        }
        
    输出结果
    2016-09-01 17:05:57.235 KVC[3467:249694] array:(
        data1,
        data2,
        data3
    )
    
    

    可以仿照代码2.1进行代码验证。由上边底层执行过程不难看出:KVC性能略逊于访问器和实例变量,但是灵活性高,视情况选择。

    说明:

    1、必要的话进行数据类型转换:KVC对应非对象类型进行自动数据类型转换,下文做详细说明。
    2、方法accessInstanceVariablesDirectly的说明:默认返回YES,表示对象的实例变量可以直接访问。
    3、关于NSUndefinedKeyException异常的处理,下文做详细说明
    

    三、异常处理

    1、方法valueForKey:寻找不到指定Key或者KeyPath匹配的方法或变量名称会自动调用valueForUndefinedKey: 抛出NSUndefinedKeyException异常
    2、方法setValue:forKey:寻找不到指定Key或者KeyPath匹配的方法或变量名称会自动调用setValue:forUndefinedKey: 抛出NSUndefinedKeyException异常

    //NSUndefinedKeyException如下所示
     *** Terminating app due to uncaught exception 'NSUnknownKeyException', 
     reason: '[<ViewController 0x7fd60b728690> setValue:forUndefinedKey:]: 
     this class is not key value coding-compliant for the key age.'
    
    

    处理方法为重写此二者方法

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

    方法体可为空也可自定义处理

    //空处理
    - (nullable id)valueForUndefinedKey:(NSString *)key
    {
        return nil;
    }
    - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key
    {
    
    }
    
    //自定义处理
    - (nullable id)valueForUndefinedKey:(NSString *)key
    {
        if ([key isEqualToString:@"key"]) {
            //返回内容自定义
            return nil;
        }
        return nil;
    }
    - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key
    {
        if ([key isEqualToString:@"key"])
        {
            //返回内容自定义
        }
    }
    

    四、非对象类型的处理

    KVC对于基本数据类型和结构体在底层支持自动数据类型转换。根据相对的存取方法或者实例变量判端实际需要的值类型,选择NSNumber 或 NSValue 进行自动转换。
    1、NSNumber对应的基本数据类型


    14726339516105.jpg

    例如

    @property (nonatomic,assign)BOOL fail;
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        NSNumber *num = [NSNumber numberWithBool:0];
        NSLog(@"class:%@",[num class]);
        
        [self setValue:@"0" forKey:@"fail"];
        NSLog(@"fali:%d--class:%@",self.fail,[[self valueForKey:@"fail"] class]);
        }
     
     输出结果:
     2016-09-01 14:27:33.401 KVC[2672:154097] class:__NSCFBoolean
     2016-09-01 14:27:33.401 KVC[2672:154097] fali:0--class:__NSCFBoolean   
    

    2、NSValue对应的结构体类型


    14726339706102.jpg

    例如

    @property (nonatomic,assign)CGPoint point;
    
        NSValue *value = [NSValue valueWithCGPoint:CGPointMake(1, 1)];
        NSLog(@"class:%@",[value class]);
    
        [self setValue:value forKey:@"point"];
        NSLog(@"fali:%@--class:%@",NSStringFromCGPoint(self.point) ,[[self valueForKey:@"point"] class]);
        
    输出结果:
    2016-09-01 14:40:23.599 KVC[2751:163036] class:NSConcreteValue
    2016-09-01 14:40:23.599 KVC[2751:163036] fali:{1, 1}--class:NSConcreteValue
    

    3、注意事项
    对非对象类型的属性设置nil空值,底层调用setNilValueForKey:,然后抛出NSInvalidArgumentException异常
    例如

     [self setValue:nil forKey:@"fail"];
     //或
     [self setValue:nil forKey:@"point"];
     
     异常:
     *** Terminating app due to uncaught exception 'NSInvalidArgumentException', 
     reason: '[<ViewController 0x7fd769484b90> setNilValueForKey]: 
     could not set nil as the value for the key fail.'
     
    

    解决方法是重写该方法setNilValueForKey:,方法可空也可自定义处理,例如

    -(void)setNilValueForKey:(NSString *)key
    {
        //自定义内容
        if ([key isEqualToString:@"fail"])
        {
            [self setValue:[NSNumber numberWithBool:0] forKey:@"fail"];
        }
        if ([key isEqualToString:@"point"])
        {
            [self setValue:[NSValue valueWithCGPoint:CGPointZero] forKey:@"point"];
        }
    }
    

    五、Key-Value Validation

    这个标题就不翻译了,英文更容易理解。

    - validateValue:forKey:error:
    - validateValue:forKeyPath:error:
    

    KVC提供一套API使得属性值生效。使得对象有机会接受值、提供默认值、拒绝新值、抛出错误原因。KVC不会自动调用,需要手动调用。默认实现过程:
    1、调用validateValue:forKey:error:
    2、在对象的方法列表中匹配validate<Key>:error:
    3、如果找到则执行并返回结果
    4、如果未找到则返回YES,并赋值
    注意:set方法中禁止调用

    @property (nonatomic,assign)NSInteger age;
    
    -(BOOL)validateAge:(id *)ioValue error:(NSError **)outError
    {
        
        if (*ioValue == nil)
        {
            // 年龄大于0岁
            [self setValue:@"0" forKey:@"age"];
            return YES;
        }
        if ([*ioValue floatValue] <= 0.0)
        {
            if (outError != NULL)
            {
                NSString *errorString = NSLocalizedStringFromTable(
                                                                   @"年龄要大于0岁", @"人",
                                                                   @"年龄错误");
                NSDictionary *userInfoDict = @{ NSLocalizedDescriptionKey : errorString };
                NSError *error = [[NSError alloc] initWithDomain:@"年龄校验"
                                                            code:0
                                                        userInfo:userInfoDict];
                *outError = error;
            }
            return NO;
        }
        else
        {
            return YES;
        }
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        NSNumber *ageNum = [NSNumber numberWithInteger:0];
        NSError *error = nil;
        [self validateValue:&ageNum forKey:@"age" error:&error];
        NSLog(@"error:%@",error);
        }
        
    输出结果
    2016-09-01 15:30:29.661 KVC[3044:197432] error:Error Domain=年龄校验 Code=0 "年龄要大于0岁" UserInfo={NSLocalizedDescription=年龄要大于0岁}
    

    五、容器类

    关于KVC在容器类中的应用。容器类主要包括:NSDictionary、NSArray、NSSet三种。关于容器类的操作方法有很多,分类整理一下
    1、如果作为对象的一个属性值,那就作为对象属性处理,无论Key还是KeyPath都符合前四条中说的规则;
    2、就可变不可变来说,一般来说存什么取什么,但是可以根据需要获取相应的方法

    @property (nonatomic,assign)NSMutableArray *mutableArray;
    
    @property (nonatomic,assign)NSArray *array;
    
    - (void)viewDidLoad {
        [super viewDidLoad];
            [self setValue:[NSArray arrayWithObjects:@"zwq", nil] forKey:@"array"];
        [self setValue:[NSMutableArray arrayWithObjects:@"zwq2", nil] forKey:@"mutableArray"];
        NSLog(@"不可变:%@--%@",[[self valueForKey:@"array"] class],[[self mutableArrayValueForKey:@"array"] class]);
        NSLog(@"可变:%@--%@",[[self valueForKey:@"mutableArray"] class],[[self mutableArrayValueForKey:@"mutableArray"] class]);
        }
        
    输出结果
    2016-09-01 16:30:55.057 KVC[3328:231529] 不可变:__NSArrayI--NSKeyValueSlowMutableArray
    2016-09-01 16:30:55.057 KVC[3328:231529] 可变:__NSArrayM--NSKeyValueSlowMutableArray
    
    //KeyPath道理也是一样的
    

    3、需要单独说的是NSDictionary跟NSArray有点不一样,而且功常用一点

    //根据指定dic设置对象属性值。使用dic的key来标识属性,dic的value标识值,底层调用setValue:forKey:进行赋值。
    - (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
    
    //获取一组key的属性值,然后以NSDictionary形式返回
    - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
    

    一个常见的功能应用,获取网络数据,数据解析完毕然后赋值的时候,如果Key很多是个很麻烦的事情,但是使用setValuesForKeysWithDictionary:一行代码搞定

    //比如Model的属性
    @property (nonatomic,copy)NSString *name;
    @property (nonatomic,copy)NSString *address;
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        //比如需要解析的数据
        NSDictionary *dic =@{@"name":@"zwq",@"address":@"地球"};
        [self setValuesForKeysWithDictionary:dic];
        NSLog(@"name:%@--address:%@",self.name,self.address);
        }
        
        输出结果
        2016-09-01 16:42:47.898 KVC[3367:237574] name:zwq--address:地球
    
    

    注意:
    1、如果dic中有未定义的key那么需要进行异常处理,参考《三、异常处理》段落。
    2、容器类比如NSArray, NSSet, NSDictionary不能包含nil值,需要使用NSNull替换(一个表示nil值的单例类)
    3、方法dictionaryWithValuesForKeys:和setValuesForKeysWithDictionary:会自动转换NSNull和nil,不需要过多关注。

    4、容器类运算符
    容器类运算是valueForKeyPath:中特殊的KeyPath,运算符跟在@符号之后,格式如下图

    Paste_Image.png
    整个KeyPath以运算符为中心,分为3部分。左边的路径标识容器类(set或者array)的访问路径,中间是运算符,右边是参加运算的属性访问路径。

    暂不支持自定义运算符,总体分为三种;

    分类 内容
    基本运算符 @avg(平均值)、@count(数量)、@max(最大值)、 @min(最小值)、@sum(求和)
    对象运算符 @distinctUnionOfObjects(祛同属性值集合)、@unionOfObjects(属性值集合)
    容器运算符 @distinctUnionOfArrays()、@unionOfArrays()、@distinctUnionOfSets()

    选择其中一个演示一下,其它的运算符同理。

    //VC有一个数组属性
    @property (nonatomic,assign)NSArray *array;
    
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        //Data有一个name属性
        Data *data1 = [[Data alloc] init];
        Data *data2 = [[Data alloc] init];
        Data *data3 = [[Data alloc] init];
        data1.name=@"data1";
        data2.name=@"data2";
        data3.name=@"data3";
        
        //self.array.name
            NSArray *arr = [NSArray arrayWithObjects:data1,data2,data1, nil];
        [self setValue:arr forKey:@"array"];
    
        NSArray *distinctArr = [self valueForKeyPath:@"array.@distinctUnionOfObjects.name"];
        NSLog(@"distinctArr:%@",distinctArr);
        
        NSArray *undistinctArr = [self valueForKeyPath:@"array.@unionOfObjects.name"];
        NSLog(@"undistinctArr:%@",undistinctArr);
        }
        
    输出结果
    2016-09-01 17:17:59.049 KVC[3507:256556] distinctArr:(
        data1,
        data2
    )
    2016-09-01 17:17:59.050 KVC[3507:256556] undistinctArr:(
        data1,
        data2,
        data1
    )
    
    

    以上问本人自己学习感悟,理解并整理。更多内容请查看官方文档

    相关文章

      网友评论

        本文标题:Key-Value Coding(键值编码)

        本文链接:https://www.haomeiwen.com/subject/yxfhettx.html