美文网首页
我不知道的KVC

我不知道的KVC

作者: 蜂猴 | 来源:发表于2016-07-20 18:24 被阅读75次

    关于KVC应该不用介绍了,通过以下方法:

    - (id)valueForKey:(NSString *)key; 
    - (void)setValue:(id)value forKey:(NSString *)key; 
    - (id)valueForKeyPath:(NSString *)keyPath; 
    - (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
    

    我们可以给对象的属性赋值或者读取属性值,通过配合runTime可以有很多妙用。

    以下几点是我之前学习过程中有偏差的或者没有接触过的:

    Key与KeyPath

    举一个例子:Person类有一个name和father的属性,其中father一个一个Person类的对象。
    为了拿到father的name,只用key,可能需要以下这些步骤:

        Person *p2 = [[Person alloc] init];
        p2.name = @"老张";
        
        Person *p1 = [[Person alloc] init];
        p1.name = @"小明";
        p1.father = p2;
        
        Person *father = [p1 valueForKey:@"father"];
        NSString *fatherName = [father valueForKey:@"name"]; 
        NSString *fatherName2 = [p1 valueForKeyPath:@"father.name"];
    

    测试结果如下:

    **(lldb) po fatherName**
    老张
    
    **(lldb) po fatherName2**
    老张
    

    很显然,KeyPath的方法会方便很多,简单来说就是KeyPath支持点语法来访问属性中的属性。

    KVC原理

    关于KVC的实现原理及一些底层的流程不是非常了解,找了一篇文章,讲的还算比较详细:
    KVC之-setValue:forKey:方法实现原理与验证
    里面主要讲了KVC对于Key的底层判断处理和一些关于Value类型转换的事。
    总结一下还是分成以下几种情况:

    • 1 如果key是一个属性名称,那么KVC自动查找该属性的setter方法并进行调用。如果找不到set方法(默认合成,声明为dynmaic可能找不到):
      (1)没有相同命名形式的实例变量,会调用setValueForUndefinedKey方法。
      (2)有相同命名形式的实例变量,进行KVC赋值。
    • 2 如果Key是一个实例变量,寻找是否有相同命名形式的实例变量,
      (1)有则进行赋值。
      (2)没有则调用setValueForUndefinedKey方法。
    • 3 如果[setValue:nil forKey:@"someKey"],使用空对象进行KVC,则调用setNilValueForKey:方法
    • 4 如果KVC方法的参数类型是NSNumber或NSValue的对应的基本类型,先把它转换为基本数据类,再执行方法,传入转换后的数据。比如属性为:
    @property (nonatomic, assign) int age;
    

    那么

        [aUser setValue:@(20) forKey:@"age"];
    

    的执行也没有问题,内部会进行基本数据的转换。
    最后解释下相同命名形式:_<key>,_is<Key>,<key>,is<Key>都可以算相同命名形式。

    键值验证

    KVC提供了验证Key和Value的方法:

    -(BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError
    
    - (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError * _Nullable __autoreleasing *)outError
    

    键值验证,它不像名称理解起来一样可以判断是否存在“键”这样的行为。
    因为如果不存在键基本都会跑到setValueForUndefinedKey方法。所以这一块的真正作用我也不是非常明白,这一块的资料也不是很多。
    按理解就是我们在调用validateValue:ForKey方法的时候,我们能有机会对value做一些特殊的判断操作等。以下参考:Objc KVV

    比如一个User类,有个name属性,理论上来说setValue:@"任何字符串" forKey:“name” 都能对Name进行赋值,但是实际情况可能比较复杂,比如我们想要对英文字符串首字母做大写处理,对于字符串长度也希望可以得到一定控制。那么这种情况下键值验证就派上大用场了。

    • 先说明一点,就是在外部调用validateValue:forKey:方法时,会直接调用validate<Key>:error:方法进行验证,如果找不到这个方法的实现,那么return YES。

    官方文档:The default implementation of this method searches the class of the receiver for a validator method whose name matches the pattern -validate<Key>:error:. If such a method is found it is invoked and the result is returned. If no such method is found, YES is returned.

    验证一下,在User类中针对name声明一个方法:

    - (BOOL)validateName:(id *)name error:(NSError **)error
    {
        if (*name == nil) {
            return NO;
        }
        else if([*name length] > 10)
        {
            return NO;
        }
        else {
            *name = [*name localizedUppercaseString];
            return YES;
        }
    }
    

    关于error没有处理,意思到了就行。
    在实际使用中我们可以这样:

        NSError *error = nil;
        NSString *name = @"shdkjahfkjsdahjfkdkjfshdk";
        if ([aUser validateValue:&name forKey:@"name" error:&error]) {
            [aUser setValue:name forKey:@"name"];
            NSLog(@"%@",[aUser valueForKey:@"name"]);
        }
    
        NSString *name2 = @"fenghou";
        if ([aUser validateValue:&name2 forKey:@"name" error:&error]) {
            [aUser setValue:name2 forKey:@"name"];
            NSLog(@"%@",[aUser valueForKey:@"name"]);
        }
    

    name太长,根据validateName:error:方法应该是返回NO的,而name2则全部变成大写。运行测试一下:

    **2016-07-20 15:25:52.018 RunTimePlayGround[85433:19941715] FENGHOU**
    

    和预计的一样。

    键值验证用的非常少,几乎见不到,因为以上这些工作在set方法里就可以做到。所以适用场景不是很广,就学习下。

    集合运算符

    以下内容参考官方文档

    集合运算符是一种特殊的keyPath,所以只能用在setValue:forKeyPath:方法中。
    使用结构如下:

    集合运算KeyPath结构图
    LeftKeyPath

    LeftKeyPath表示该方法调用者的容器属性,如果调用者本身就是一个容器类,那么可以省略。举个例子,比如在一个VC中有一个userArray属性,
    如果想要使用运算符的方法,有两种方法:

    * 1 [self valueForKeyPath:@"userArray.@avg.age"]
    * 2 [self.userArray valueForKeyPath:@"@avg.age"]
    

    两种方法的结果都是一样的,但是第二种方法的LeftKeyPath是可以省略的。

    RightKeyPath

    再讲一下Right Key Path,它用来表示集合中元素想要被操作的那个属性,比如上文中的userArray里头包含了很多user对象,但是我们希望操作是针对user对象的age属性,那么RightKeyPath就是age,如果想要进行score(得分)的操作,RightKeyPath就是score。简单点说就是RightKeyPath就是要操作的属性,不能为空(除了@count运算符后面可以不跟RightKeyPath)。

    简单运算符

    简单运算符包含一下5个:@avg,@count,@max,@min,@sum,目前还不支持自定义。所做的字面操作都和字面意思一样,按顺序就是:取平均,取总数,最大值,最小值,总合。

    对前面的LeftKeyPath和RightKeyPath有所了解之后,下面运用简单运算符也不算难:

        User *user1 = [[User alloc] initWithAge:10];
        User *user2 = [[User alloc] initWithAge:2];
        User *user3 = [[User alloc] initWithAge:37];
        User *user4 = [[User alloc] initWithAge:69];
        self.userArray = @[user1, user2, user3, user4];
        NSLog(@"%@",[self valueForKeyPath:@"userArray.@avg.age"]);
        NSLog(@"%@",[self valueForKeyPath:@"userArray.@count.age"]);
        NSLog(@"%@",[self valueForKeyPath:@"userArray.@max.age"]);
        NSLog(@"%@",[self valueForKeyPath:@"userArray.@min.age"]);
        NSLog(@"%@",[self valueForKeyPath:@"userArray.@sum.age"]);
    

    打印如下:

    2016-07-20 17:15:14.228 RunTimePlayGround[86511:20234568] 29.5
    2016-07-20 17:15:14.228 RunTimePlayGround[86511:20234568] 4
    2016-07-20 17:15:14.229 RunTimePlayGround[86511:20234568] 69
    2016-07-20 17:15:14.229 RunTimePlayGround[86511:20234568] 2
    2016-07-20 17:15:14.229 RunTimePlayGround[86511:20234568] 118
    
    • @avg操作符会将待计算对象转为double值进行计算平均,然后返回一个NSNumber对象。
    • @count就是计算待计算对象的数量,可以不需要RightKeyPath。
    • @max和@min通过对象的compare方法来进行比较大小,不支持compare方法的对象会崩溃。如果待计算对象为数字类型,返回NSNumber,其余返回待计算对象本身类型。
    • @sum,与@avg类似,将待计算对象转为double进加和,返回NSNumber对象。

    测试了一下,数字类型的对象以上5个计算符都支持,NSDate支持@max,@min,@count。
    字符串类型与NSDate比较相像,但是特殊的一点在于如果字符串首字符是数字或者字符串是纯数字那么也能进行@avg和@sum的计算,与@avg和@sum的操作方式一致:先转成double。

    对象运算符

    有两种运算符@distinctUnionOfObjects和@unionOfObjects,使用类似于简单运算符,只是功能不一样。
    两种运算符都是将所有待运算对象重新打包成一个新的数组进行返回,区别在于@unionOfObjects不会剔除那些重复元素,@distinctUnionOfObjects则会做去重处理,直接看例子:

        User *user1 = [[User alloc] initWithAge:10 birth:birth1];
        User *user2 = [[User alloc] initWithAge:10 birth:birth2];
        User *user3 = [[User alloc] initWithAge:37 birth:birth3];
        User *user4 = [[User alloc] initWithAge:69 birth:birth4];
        self.userArray = @[user1, user2, user3, user4];
        NSArray *distinctUnion = [self valueForKeyPath:@"userArray.@distinctUnionOfObjects.age"];
        NSArray *unionArray = [self valueForKeyPath:@"userArray.@unionOfObjects.age"];
    

    结果如下:

    **(lldb) po distinctUnion**
    <__NSArrayI 0x7fb789d56e70>(
    10,
    69,
    37
    )
    
    **(lldb) po unionArray**
    <__NSArrayM 0x7fb789d56ec0>(
    10,
    10,
    37,
    69
    )
    

    可以明显看到两种运算符的区别。

    容器运算符

    包含以下三种运算符:
    @distinctUnionOfArrays
    @unionOfArrays
    @distinctUnionOfSets
    其实进行的运算和对象运算符差不多,只不过适用于容器中的容器这种情况,直接举例子好了:

        User *user1 = [[User alloc] initWithAge:10 ];
        User *user2 = [[User alloc] initWithAge:10 ];
        User *user3 = [[User alloc] initWithAge:37 ];
        User *user4 = [[User alloc] initWithAge:69 ];
        self.userArray1 = @[user1, user2, user3, user4];
        
        User *user5 = [[User alloc] initWithAge:69 ];
        User *user6 = [[User alloc] initWithAge:11 ];
        User *user7 = [[User alloc] initWithAge:23 ];
        User *user8 = [[User alloc] initWithAge:18 ];
        self.userArray2 = @[user5,user6,user7,user8];
        NSArray *array3 = @[self.userArray1, self.userArray2];
        NSArray *distinctUnionArray = [array3 valueForKeyPath:@"@distinctUnionOfArrays.age"];
        NSArray *unionArray = [array3 valueForKeyPath:@"@unionOfArrays.age"];
    

    结果如下:

    **(lldb) po distinctUnionArray**
    <__NSArrayI 0x7fa150d4af60>(
    18,
    10,
    23,
    37,
    11,
    69
    )
    
    **(lldb) po unionArray**
    <__NSArrayM 0x7fa150d15590>(
    10,
    10,
    37,
    69,
    69,
    11,
    23,
    18
    )
    

    前两个针对的集合是Arrays,后一个针对的集合是Sets。 因为Sets中的元素本身就是唯一的,所以没有对应的@unionOfSets运算符,只有@distinctUnionOfSets

    相关文章

      网友评论

          本文标题:我不知道的KVC

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