KVC

作者: 瞬间完善 | 来源:发表于2020-02-16 14:59 被阅读0次

    我们可以通过苹果官方文档看到KVC的解释:

    键值编码是NSKeyValueCoding非正式协议支持的一种机制,对象采用这种机制来提供对其属性的间接访问。当对象符合键值编码时,可通过简洁,统一的消息传递接口通过字符串参数访问其属性,这种间接访问机制补充了实例变量及其关联的访问器方法提供的直接访问。

    通常,您使用访问器方法来访问对象的属性。一个get访问器(或getter)从一个属性返回值,一个set访问器(或setter)给一个属性设置值。在Objective-C中,您还可以直接访问属性的基础实例变量。以任何一种方式访问​​对象属性都很简单,但是需要调用特定于属性的方法或变量名。随着属性列表的增加或更改,访问这些属性的代码也必须如此。相反,与键值编码兼容的对象提供了一个简单的消息传递接口,该接口在其所有属性之间都是一致的。

    键值编码是许多其他Cocoa技术的基础概念,例如键值观察,Cocoa绑定,Core DataAppleScript-ability。在某些情况下,键值编码还可以帮助简化代码。

    在实现了访问器方法的类中,使用点语法和KVC访问对象其实差别不大,二者可以任意混用。但是没有访问器方法的类中,点语法无法使用,这时KVC就有优势了。

    1、访问对象的属性

    属性:这些是简单的值,例如标量,字符串或布尔值。值对象(例如NSNumber)和其他不可变类型(例如NSColor)也被视为属性。
    一对一的关系:这些是具有自己属性的可变对象。对象的属性可以更改,而无需更改对象本身。例如,银行帐户对象可能具有所有者属性,该属性是Person对象的实例,而Person对象本身具有address属性。所有者的地址可以更改,而无需更改银行帐户持有的所有者属性。
    一对多关系:这些是集合对象。尽管也可以使用自定义集合类,但是通常使用NSArrayNSSet的实例来保存此类集合。

    @interface YXPerson : NSObject
     
    @property (nonatomic) NSNumber* accountNumber;              // 一个属性
    @property (nonatomic, strong) YXStudent *student;           // 一对一的关系
    @property (nonatomic) NSArray< Transaction* >* transactions; // 一对多的关系
     
    @end
    

    为了维护封装,对象通常为其接口上的属性提供访问器方法。 对象的作者可以显式地编写这些方法,也可以依靠编译器自动合成它们。 无论哪种方式,使用这些访问器之一的代码作者都必须在编译属性名称之前将其写入代码。 访问器方法的名称成为使用它的代码的静态部分。 例如:

    // ✅ 在YXPerson.h中定义一个accountNumber属性
    @interface YXPerson : NSObject
    @property (nonatomic, assign) NSNumber * accountNumber;
    @end
    ************************
    // ✅ 在YXPerson.m中
    - (NSNumber*) setAccountNumber{
        return self. accountNumber;
    }
    ************************
    // ✅ 在使用的地方:
    [person setAccountNumber:@19];
    NSLog(@"accountNumber - %@",person. accountNumber);
    ************************
    // ✅ 输出
    2020-02-15 18:53:32.792859+0800 KVC简用[16886:179545] accountNumber - 19
    

    这是最直接的,但缺乏灵活性。 另一方面,符合键值编码的对象提供了一种更通用的机制,可以使用字符串标识符访问对象的属性。

    1.1、使用KeyKeyPaths径识别对象的属性

    key是标识特定属性的字符串。 通常,按照约定,代表属性的键是该属性本身在代码中出现的名称。key必须使用ASCII编码,不能包含空格,并且通常以小写字母开头(尽管有例外,例如在许多类中找到的URL属性)。

    由于上面代码中的YXPerson类符合键值编码,因此它可以识别这些key键的accountNumberstudenttransactions,这是其属性的名称。 您可以通过其键设置值,而不是调用setAccountNumber:方法:

    [person setValue:@19 forKey:@"accountNumber"];
    
    1.2、使用key获取属性的值

    当对象采用NSKeyValueCoding协议时,它符合键值编码。继承自NSObject的对象(提供了该协议的基本方法的默认实现)会自动采用具有某些默认行为的该协议。这样的对象至少实现以下基于键的基本getter

    • valueForKey:-返回由key参数命名的属性的值。如果根据访问者搜索模式中描述的规则找不到由关键字命名的属性,则该对象将向自身发送valueForUndefinedKey:消息。 valueForUndefinedKey:的默认实现抛出了NSUndefinedKeyException,但是子类可以重写此行为并更优雅地处理这种情况。

    • valueForKeyPath:-返回相对于接收者的指定密钥路径的值。密钥路径序列中不符合特定键的键值编码的任何对象(即valueForKey:的默认实现无法找到访问器方法)都接收到valueForUndefinedKey:消息。

    • dictionaryWithValuesForKeys:-返回相对于接收者的键数组的值。该方法为数组中的每个键调用valueForKey:。返回的NSDictionary包含数组中所有键的值。

    集合对象(例如NSArrayNSSetNSDictionary)不能包含nil作为值。 而是使用NSNull对象表示nil值。NSNull提供了单个实例,表示对象属性的nil值。 dictionaryWithValuesForKeys:和相关的setValuesForKeysWithDictionary:的默认实现会自动在NSNull(在dictionary参数中)和nil(在存储的属性中)之间转换。

    1.3、使用key设置属性的值

    getter一样,与键值编码兼容的对象还根据NSObject中提供的NSKeyValueCoding协议的实现,为一小组具有默认行为的定义setter

    • setValue:forKey:-将相对于接收消息的对象的指定键的值设置为给定值。 setValue:forKey:的默认实现:将表示标量和结构的NSNumberNSValue对象自动解包,并将它们分配给属性。
    • 如果该对象没有对应的key的属性,则该对象将向自身发送setValue:forUndefinedKey:消息。 setValue:forUndefinedKey:的默认实现抛出一个NSUndefinedKeyException。但是,子类可以重写此方法以自定义方式处理请求。
    • setValue:forKeyPath:-在相对于接收者的指定键路径处设置给定值。key路径序列中不符合特定键的键值编码的任何对象都会收到setValue:forUndefinedKey:消息。
    • setValuesForKeysWithDictionary:-使用字典键标识属性,使用指定字典中的值设置接收器的属性。默认实现为每个键值对调用setValue:forKey :,并根据需要用nil代替NSNull对象。

    在默认实现中,当您尝试将非对象属性设置为nil值时,符合键值编码的对象会向自身发送setNilValueForKey:消息。 setNilValueForKey:的默认实现抛出NSInvalidArgumentException,但是对象重写这个方法,以设置默认值或标记值。
    例如:

    YXPerson *person = [[YXPerson alloc] init];
        
        // 1:Key-Value Coding (KVC) : 基本类型
        [person setValue:@19 forKey:@"accountNumber"];
        
        NSLog(@"person - %@",person.accountNumber);
        
    // ✅ KeyPath赋值
        YXStudent *student = [[YXStudent alloc] init];
        person.student   = student;
        [person setValue:@"KP" forKeyPath:@"student.name"];
        NSLog(@"student - %@ ",[person valueForKeyPath:@"student.name"]);
    **********************
    // ✅ 输出
    2020-02-15 19:46:54.164662+0800 KVC简用[20478:221482] person - 19
    2020-02-15 19:46:54.164921+0800 KVC简用[20478:221482] student - KP
    

    2、访问集合属性

    就像访问和设置其他属性一样,您也可以使用valueForKey:setValue:forKey:来访问和设置集合属性的值。但是,当您要操纵这些集合的内容时,通常使用协议定义的可变代理方法最有效。

    该协议定义了三种不同的代理对象访问代理方法,每种方法都有一个键和一个键路径变量:

    • mutableArrayValueForKey:mutableArrayValueForKeyPath:它们返回行为类似于NSMutableArray对象的代理对象。
    • mutableSetValueForKey:mutableSetValueForKeyPath:它们返回行为类似于NSMutableSet对象的代理对象。
    • mutableOrderedSetValueForKey:mutableOrderedSetValueForKeyPath:它们返回行为类似于NSMutableOrderedSet对象的代理象。
      例如:
    // 2: KVC - 集合类型
        person.array = @[@"1",@"2",@"3"];
        // ✅ 不可变数组
        NSArray *ary = [person valueForKey:@"array"];
        ary = @[@"100",@"2",@"3"];
        [person setValue:ary forKey:@"array"];
        NSLog(@"%@",[person valueForKey:@"array"]);
        // ✅ 可变数组
        NSMutableArray *muAry = [person mutableArrayValueForKey:@"array"];
        muAry[0] = @"200";
        NSLog(@"%@",[person valueForKey:@"array"]);
    *************************
    // ✅ 输出
    2020-02-16 11:14:12.620737+0800 KVC简用[3041:31480] (
        100,
        2,
        3
    )
    2020-02-16 11:14:12.623391+0800 KVC简用[3041:31480] (
        200,
        2,
        3
    )
    

    3、集合运算符

    当您发送与键值编码兼容的对象valueForKeyPath:消息时,可以将集合运算符嵌入到键路径中。集合运算符是一小部分关键字之一,其后带有一个at符号(@),该符号指定getter在返回数据之前应执行的操作以某种方式处理数据。由NSObject提供的valueForKeyPath:的默认实现会实现此行为。

    • @avg.属性名求集合中对象某个属性的平均值。
    • @count求集合中对象个数。
    • @max.属性名求集合中对象某个属性的最大值。
    • @min.属性名求集合中对象某个属性的最小值。
    • @sum.属性名求集合中对象某个属性的和。
    • @distinctUnionOfObjects.属性名取出集合中所有对象某个属性的值,并将这些值存入一个新的数组并返回。这个操作去重。
    • @unionOfObjects.属性名取出集合中所有对象某个属性的值,并将这些值存入一个新的数组并返回。这个操作不去重。
    • @distinctUnionOfArrays.属性名取出嵌套集合(集合嵌套集合)中所有对象某个属性的值,并返回一个新的数组。这个操作去重。
    • @unionOfArrays.属性名取出嵌套集合(集合嵌套集合)中所有对象某个属性的值,并返回一个新的数组。这个操作不去重。
    • @distinctUnionOfSets.属性名返回值是个一个NSSet效果和distinctUnionOfArrays一样。

    4、类型转换

    当您调用协议的一种getters,例如valueForKey:时,默认实现将根据访问者搜索模式中描述的规则来确定为指定键提供值的特定访问器方法或实例变量。 如果返回值不是对象,则getter使用此值初始化NSNumber对象(用于标量)或NSValue对象(用于结构体),并返回该值。

    类似地,默认情况下,使用setValue:forKey之类的setter:在给定特定键的情况下,确定属性的访问器或实例变量所需的数据类型。 如果数据类型不是对象,则设置器首先将适当的<type> Value消息发送到传入值对象以提取基础数据,然后存储该数据。

    WX20200213-180329@2x.png
    WX20200213-180313@2x.png

    4.1、自定义结构体类型的转换

    typedef struct {
        float x, y, z;
    } ThreeFloats;
     
    @interface MyClass
    @property (nonatomic) ThreeFloats threeFloats;
    @end
    

    通过KVC取值的时候,通过NSValue获取

    NSValue* result = [myClass valueForKey:@"threeFloats"];
    

    通过NSValue来进行包装一下,然后再通过KVC赋值。

    ThreeFloats floats = {1., 2., 3.};
    NSValue* value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
    [myClass setValue:value forKey:@"threeFloats"];
    

    5、KVC的底层原理

    5.1、getter方法查找

    当一个对象调用valueForKey:方法取值的时候,他的内部执行以下过程。

    • 1.在实例中搜索找到具有名称的第一个访问器方法get<Key><key>is<Key>,或者_<key>,按照这个顺序。如果找到,则调用它并执行步骤5。否则,请继续下一步。
    • 2.判断是否是数组,如果是数组则查找countOf<Key>,objectIn<Key>AtIndex:<key>AtIndexes,并返回一个新的数组。否则就执行步骤3。
    • 3.判断是否是NSSet,查找countOf<Key>,enumeratorOf<Key>memberOf<Key>: 。否则就执行步骤4。
    • 4.调用accessInstanceVariablesDirectly方法,判断是否启用实例变量的查找,默认是YES,也就是启用,当返回为YES时,将按照这个_<key>,_is<Key>, <key>, oris<Key>,来一次查找。我们可以通过重写这个方法来禁用实例变量的查找。
    • 5.如果检索到的属性值是对象指针,则只需返回结果。如果该值是NSNumber支持的标量类型,则将其存储在NSNumber实例中并返回它。如果结果是NSNumber不支持的标量类型,请转换为NSValue对象并返回该对象。
    • 6.如果所有的方法均失败,则调用valueForUndefinedKey:。 默认情况下,这会抛出一个异常,但是NSObject的子类可以通过重写这个方法,来定制一些特性的功能。
      验证:
      第一点:
    // ✅ YXPerson.h
    @property (nonatomic, copy) NSString *name;
    **********************
    // ✅ YXPerson.m,可以每一次注释一个,看输出
    - (NSString *)getName{
        return NSStringFromSelector(_cmd);
    }
    
    - (NSString *)name{
        return NSStringFromSelector(_cmd);
    }
    
    - (NSString *)isName{
        return NSStringFromSelector(_cmd);
    }
    
    - (NSString *)_name{
        return NSStringFromSelector(_cmd);
    }
    **********************
    // ✅ 取值
    person->_name = @"_name";
    person->_isName = @"_isName";
    person->name = @"name";
    person->isName = @"isName";
        
    NSLog(@"取值:%@",[person valueForKey:@"name"]);
    **********************
    // ✅ 输出
    2020-02-16 12:36:17.589496+0800 KVC简用[8398:91066] 取值:getName
    2020-02-16 12:36:17.589496+0800 KVC简用[8398:91066] 取值:name
    2020-02-16 12:36:17.589496+0800 KVC简用[8398:91066] 取值:isName
    2020-02-16 12:36:17.589496+0800 KVC简用[8398:91066] 取值:_name
    

    第二点:

    // ✅ YXPerson.h
    @property (nonatomic, strong) NSArray *array;
    **********************
    // ✅ YXPerson.m
    - (NSInteger)countOfNames{
        return self.array.count;
    }
    - (id)objectInNamesAtIndex:(NSUInteger)index{
        return _array[index];
    }
    **********************
    // ✅ 取值
    person.array = @[@"1",@"2",@"3"];
    NSLog(@"%@",[person valueForKey:@"names"]);
    **********************
    // ✅ 输出
    2020-02-16 13:04:10.290894+0800 KVC简用[10284:112668] (
        1,
        2,
        3
    )
    

    第三点:

    // ✅ YXPerson.h
    @property (nonatomic, strong) NSArray *array;
    @property (nonatomic, strong) NSSet   *set;
    **********************
    // ✅ YXPerson.m
    // 个数
    - (NSUInteger)countOfBooks{
        NSLog(@"%s",__func__);
        return [self.set count];
    }
    
    // 是否包含这个成员对象
    - (id)memberOfBooks:(id)object {
        NSLog(@"%s",__func__);
        return [self.set containsObject:object] ? object : nil;
    }
    
    // 迭代器
    - (id)enumeratorOfBooks {
        // objectEnumerator
        NSLog(@"来了 迭代编译");
        return [self.array reverseObjectEnumerator];
    }
    **********************
    // ✅ 取值
    person.array = @[@"pen0", @"pen1", @"pen2", @"pen3"];
    // set 集合
    person.set = [NSSet setWithArray:person.array];
    NSSet *set = [person valueForKey:@"books"];
    [set enumerateObjectsUsingBlock:^(id  _Nonnull obj, BOOL * _Nonnull stop) {
    NSLog(@"set遍历 %@",obj);
        }];
    **********************
    // ✅ 输出
    2020-02-16 13:14:56.213050+0800 KVC简用[11040:122217] -[YXPerson countOfBooks]
    2020-02-16 13:14:56.213228+0800 KVC简用[11040:122217] -[YXPerson countOfBooks]
    2020-02-16 13:14:56.213361+0800 KVC简用[11040:122217] 来了 迭代编译
    2020-02-16 13:14:56.213494+0800 KVC简用[11040:122217] set遍历 pen3
    2020-02-16 13:14:56.213610+0800 KVC简用[11040:122217] set遍历 pen2
    2020-02-16 13:14:56.213720+0800 KVC简用[11040:122217] set遍历 pen1
    2020-02-16 13:14:56.213825+0800 KVC简用[11040:122217] set遍历 pen0
    
    5.2、setter方法查找

    setValue:forKey:的默认实现(给定键和值参数作为输入),尝试将名为key的属性设置为value,在使用这个方法设置值时,对象的内部会经历以下流程。

    • 1.按该顺序查找名为set <Key>:_set <Key>setIs<Key>的第一个访问器。 如果找到,请使用输入值调用它并完成。

    • 2.如果没有找到setter访问器,并且类方法accessInstanceVariablesDirectly返回YES,则按该顺序查找名称类似于_ <key>_ is <Key><key>is <Key>的实例变量。 如果找到,直接用输入值设置变量并完成操作。

    • 3.在找不到访问器或实例变量后,调用setValue:forUndefinedKey:。 默认情况下,这会抛出一个异常,但是NSObject的子类可以通过重写这个方法来提供特定的操作。
      验证:
      第一点:

    // ✅ YXPerson.h
    @property (nonatomic, copy) NSString *name;
    **********************
    // ✅ YXPerson.m
    //MARK: - setKey. 的流程分析
    - (void)setName:(NSString *)name{
        NSLog(@"%s - %@",__func__,name);
    }
    
    - (void)_setName:(NSString *)name{
        NSLog(@"%s - %@",__func__,name);
    }
    
    - (void)setIsName:(NSString *)name{
        NSLog(@"%s - %@",__func__,name);
    }
    **********************
    // ✅ 取值
    [person setValue:@"YX" forKey:@"name"];
    **********************
    // ✅ 输出
    2020-02-16 13:21:58.807533+0800 KVC简用[11508:128377] -[YXPerson setName:] - YX
    2020-02-16 13:21:58.807533+0800 KVC简用[11508:128377] -[YXPerson _setName:] - YX
    2020-02-16 13:21:58.807533+0800 KVC简用[11508:128377] -[YXPerson setIsName:] - YX
    

    第二点:

    // ✅ YXPerson.h
    @interface YXPerson : NSObject{
        @public
        NSString *_name;
        NSString *_isName;
        NSString *name;
        NSString *isName;
    }
    **********************
    // ✅ YXPerson.m
    - (void)setValue:(id)value forUndefinedKey:(NSString *)key {
        NSLog(@"%@ is Undefined", key);
    }
    **********************
    // ✅ 取值
    [person setValue:@"YX" forKey:@"name"];
     NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
     NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
     NSLog(@"%@-%@",person->name,person->isName);
     NSLog(@"%@",person->isName);
    **********************
    // ✅ 1. 四个成员变量_name,_isName,name,isName
    2020-02-16 13:33:03.731309+0800 KVC简用[12265:138431] YX-(null)-(null)-(null)
    2020-02-16 13:33:03.731481+0800 KVC简用[12265:138431] (null)-(null)-(null)
    2020-02-16 13:33:03.731596+0800 KVC简用[12265:138431] (null)-(null)
    2020-02-16 13:33:03.731693+0800 KVC简用[12265:138431] (null)
    // ✅ 2. 三个成员变量_isName,name,isName
    2020-02-16 13:35:21.183230+0800 KVC简用[12445:141103] YX-(null)-(null)
    2020-02-16 13:35:21.183438+0800 KVC简用[12445:141103] (null)-(null)
    2020-02-16 13:35:21.183590+0800 KVC简用[12445:141103] (null)
    // ✅ 3. 两个成员变量name,isName
    2020-02-16 13:36:00.207210+0800 KVC简用[12502:141939] YX-(null)
    2020-02-16 13:36:00.207396+0800 KVC简用[12502:141939] (null)
    // ✅ 4. 一个成员变量isName
    2020-02-16 13:36:51.657983+0800 KVC简用[12575:142914] YX
    **********************
    // ✅ 四个成员变量都不在
    2020-02-16 13:37:58.726567+0800 KVC简用[12670:144203] name is Undefined
    

    参考:

    KVC底层原理

    相关文章

      网友评论

          本文标题:KVC

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