Objective-C中的KVC

Posted __Sunshine_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Objective-C中的KVC相关的知识,希望对你有一定的参考价值。

本文主要介绍KVC的以下几部分:

1、概念
KVC, 即 Key-Value Coding(有翻译成键值编码)
是通过字符串来识别属性名,来间接访问对象属性的机制,与调用setter、getter方法的直接访问不同。
作用是简化代码,可以动态地设置和读取属性。

主要的KVO方法声明在OC的非正式协议 NSKeyValueCoding 中

@interface NSObject(NSKeyValueCoding)

KVC中用来划分属性的一些术语:
attribute表示只有一个简单的值的属性,如数值、字符串、Boolean值、NSNumber等;
对一关系(to-one relationship)表示拥有自己属性的对象,如NSView;
对多关系(to-many relationship)表示由集合组成的属性,如NSArray、NSSet。

key 与 key paths
key 是用来识别对象的属性的字符串,它可以与对象的实例变量名相同,也可以与访问方法名(setter,getter)相同。key要用ASCII编码,以小写字母开头,不能包含空格。如amount, page, name.
key paths是用来识别多层属性的,如对象person中有属性为book对象,book对象中有int型属性page,则对person对象使用的字符串应该用“.”分隔属性名,如:book.page

2、基本用法

1)用KVC获取属性值:

- (id)valueForKey:(NSString *)key;
// 依字符串key来获取对应的属性的值,返回值是能表示该属性值的id
- (id)valueForKeyPath:(NSString *)keyPath;
// 依字符串keyPath来获取符合路径的属性(多层),返回值是能表示该属性值的id
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;
// 获取一组属性值。keys为要获取的多个属性名的数组。返回值是个字典,其键是keys中的元素,其值是对应的值。

例子如下:

//  Book.h
#import <Foundation/Foundation.h>
#import "Author.h"
@interface Book : NSObject                   //声明Book类代表书
@property (nonatomic, assign) int page;      //属性,表页码
@property (nonatomic, assign) double price;  //属性,表价格
@property (nonatomic, strong) Author *author;//属性,表作者
@end
//  Book.m
#import "Book.h"
@implementation Book
@end
//  Author.h
#import <Foundation/Foundation.h>
@interface Author : NSObject                 //声明Author类表作者
@property (nonatomic, strong) NSString *name;//属性,表名字
@end
//  Author.m
#import "Author.h"
@implementation Author
@end
//  main.m
#import <Foundation/Foundation.h>
#import "Book.h"
#import "Author.h"
int main(int argc, const char * argv[]) 
    @autoreleasepool 

        // 创建对象
        Book *book = [Book new];
        Author *author = [Author new];

        // 对象属性赋值
        author.name = @"wangxiaobo";
        book.page = 200;
        book.price = 58.5;
        book.author = author;

        // 用 KVC 获取属性值

        // (1)获取 book 的 page 值
        int thePage = [[book valueForKey:@"page"] intValue];
        // thePage == 200

        // (2)获取 book 的 author 对象的 name 值
        NSString *theName = [book valueForKeyPath:@"author.name"];
        // theName == @"wangxiaobo"

        // (3)获取多个属性值,设置要获取的一组属性:
        NSArray *atrArr = @[@"page", @"price"];
        // 获取到一组属性的值,返回类型为字典,属性名为key,属性值为value
        NSDictionary *artDic = [book dictionaryWithValuesForKeys:atrArr];
        // artDic == 
        //      page = 200;
        //      price = "58.5";
        // 
    
    return 0;

示意图如下:

2)用KVC设置对象的属性值

- (void)setValue:(id)value forKey:(NSString *)key;
// 将对象的key属性设置为value(注意value的类型是id)
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
// 将对象的keyPath路径下的属性设置为value
- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;
// 用字典keyedValues对对象的多个属性赋值,用其键来匹配属性名,用其值来设置属性的值
// 对于每个键值对,都会默认调用setValue: value forKey:方法

例子如下:

//(接上面的代码)
        // 用 KVC 设置属性值
        // 设置属性 page 的值
        [book setValue:@250 forKey:@"page"];
        // book.page == 250

        // 用 KVC 设置 book 的 author 的 name 的值
        [book setValue:@"luxun" forKeyPath:@"author.name"];
        // book.author.name == @"luxun"

        // 设置多个属性的值
        // 创建一个字典,key为要设置的属性名,value为对应的值
        NSDictionary *setDic = @@"page":@300, @"price":@65.5;
        // 用字典去设置多个属性的值
        [book setValuesForKeysWithDictionary:setDic];
        // book.page == 300
        // book.price == 65.5

KVC与点语法之间没什么必然的联系。用不用点语法,都可以考虑用KVC;用不用KVC,都可以考虑用点语法。在KVC中,用key路径来限定元素,返回值不会是基本数据类型;在点语法中,调用的是访问器方法(setter, getter),返回值可以是基本数据类型。

3、当属性为对多属性(如集合NSArray,NSSet等)

1)虽然在获取和设置过程中,如果属性不是对象类型的(如基本数据类型,结构体等),上面方法可以自动的包装和拆包将非对象类型与对象类型进行转换,但当想要把属性值设置为零值时,要重写调用的方法setNilValueForKey:,否则出错。重写如下:

//  Book.m
#import "Book.h"
@implementation Book
// 重写 setNilValueForKey:方法
-(void)setNilValueForKey:(NSString *)key

    // 对要设置为nil的属性名作判断
    if ([key isEqualToString:@"page"]) 
        // 如果是属性 page,则用 @0 代表要设置的值
        [self setValue:@0 forKey:@"page"];
    else if ([key isEqualToString:@"price"])
        // 如果是属性 price,则用 @0.0 代表要设置的值
        [self setValue:@0.0 forKey:@"price"];
    else
        // 如果要设置的属性是对象,则调用父类的setNilValueForKey:方法
        [super setNilValueForKey:key];
    


@end

2)setter、getter 方法最好只用在集合对象上,对于集合中的元素,应该实现另外一些方法来进行访问。如:

-countOf<Key>
应实现的方法。作用类似于 NSArray 的 count 方法。

-objectIn<Key>AtIndex: 或者-<key>AtIndexs:
应实现两者之一,分别类似于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:。

-get<Key>:range:
可实现可不实现,类似于 NSArray 的 getObjects:range: 方法。

例子如下:

//  Book.h
#import <Foundation/Foundation.h>
#import "Author.h"
@interface Book : NSObject                        //声明Book类代表书
@property (nonatomic, strong) NSArray *sellPlace; //属性,是数组,表书的销售城市
//声明countOfSellPlace方法
-(NSUInteger)countOfSellPlace;
//声明由下标获取集合属性中的元素的方法
-(id)objectInSellPlaceAtIndex:(NSUInteger)index;
@end

//  Book.m
#import "Book.h"
@implementation Book
//实现countOfSellPlace方法
-(NSUInteger)countOfSellPlace
    //调用 NSArray 的count方法,将结果返回
    return [self.sellPlace count];

//实现方法,作用是通过下标访问集合属性中的元素
-(id)objectInSellPlaceAtIndex:(NSUInteger)index
    //调用 NSArray 的 objectAtIndex:方法,将结果返回
    return [self.sellPlace objectAtIndex:index];

@end

//  main.m
#import <Foundation/Foundation.h>
#import "Book.h"
int main(int argc, const char * argv[]) 
    @autoreleasepool 
        // 创建对象
        Book *book = [Book new];
         //设置书的销售城市
        NSArray *sellPlace = @[@"BJ", @"SH", @"GZ"];
        book.sellPlace = sellPlace;     

        // 调用方法,获取 book 对象中数组 sellPlace 的元素个数
        NSUInteger cou = [book countOfSellPlace];
        // cou == 3

        // 调用方法,通过下标获取 book 对象中数组 sellPlace 的元素
        NSString *aSellPlace = [book objectInSellPlaceAtIndex:1];
        // aSellPlace == @"SH"
    
    return 0;

3)如果集合是可变的,还应实现一些方法使得可以对集合的元素进行操作。如:

• -insertObject:in<Key>AtIndex: 或者 -insert<Key>:atIndexes:
应该实现这两者之一。类似于  NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:。

• -removeObjectFrom<Key>AtIndex:或者-remove<Key>AtIndexes:
应该实现这两者之一。类似于  NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:。

• -replaceObjectIn<Key>AtIndex:withObject: 或者 -replace<Key>AtIndexes:with<Key>:
可实现可不实现。 Implement if benchmarking indicates that performance is an issue.

例子如下:

//  Author.h
#import <Foundation/Foundation.h>
@interface Author : NSObject                 //声明Author类表作者
@property (nonatomic, strong) NSString *name;//属性,表名字
@end
//  Author.m
#import "Author.h"
@implementation Author
@end

//  Book.h
#import <Foundation/Foundation.h>
#import "Author.h"
@interface Book : NSObject                              //声明Book类代表书
@property (nonatomic, strong) NSMutableArray *authors;  //可变数组,用来存储多个作者
//声明给可变对多属性插入元素的方法
-(void)insertObject:(Author *)object inAuthorsAtIndex:(NSUInteger)index;
//声明对可变对多属性删除元素的方法
-(void)removeObjectFromAuthorsAtIndex:(NSUInteger)index;
@end
//  Book.m
#import "Book.h"
@implementation Book
//实现给可变对多属性插入元素的方法
-(void)insertObject:(Author *)object inAuthorsAtIndex:(NSUInteger)index
    [self.authors insertObject:object atIndex:index];

//实现对可变对多属性删除元素的方法
-(void)removeObjectFromAuthorsAtIndex:(NSUInteger)index
    [self.authors removeObjectAtIndex:index];

@end
//  main.m
#import <Foundation/Foundation.h>
#import "Book.h"
#import "Author.h"
int main(int argc, const char * argv[]) 
    @autoreleasepool 
        // 创建book对象
        Book *book = [Book new];

        // (1)插入元素到对象的可变数组属性中:
        // 创建两个Author对象
        Author *author = [Author new];
        Author *author_2 = [Author new];
        author.name = @"wangxiaobo";
        author_2.name = @"guojingming";

        // 创建可变数组authors,里面有一个元素:author对象(用来表示第一个作者)
        NSMutableArray *authors = [NSMutableArray arrayWithObject:author];
        // 此时:authors == @[author]

        // 用 KVC 将可变数组 authors 赋值给 book 的 NSMutableArray 型属性
        [book setValue:authors forKey:@"authors"];
        // 此时:book.authors == @[author]

        // 调用已实现的插入方法将 author_2 对象插入book 的 NSMutableArray 型属性中下标为1的位置
        [book insertObject:author_2 inAuthorsAtIndex:1];
        // 此时:book.authors == @[author, author_2]
        // 原可变数组 authors 也等于 @[author, author_2]

        // (2)从对象的可变数组属性中删除元素:
        // 删除 book 对象中的可变数组 authors 的下标为1的元素
        [book removeObjectFromAuthorsAtIndex:1];
        // 此时:book.authors == @[author]
    
    return 0;

4、集合操作符(Collection Operators)

当对象属性是集合,集合操作符可以通过键路径记号(key path notation)与一个操作符,来对集合的元素进行一些操作。
集合操作符是一种特殊的键路径,是作为参数传给 valueForKeyPath: 方法。其中的操作符是以“@”开头的,操作符左边的是对象中的、用来操作的一个 array 或者 set,而操作符右边的是被操作符使用的集合的属性。格式如下:

操作符分类:
(1)简单集合操作符:返回字符串、数字、日期等,应用NSNumber型对象接收。
(2)对象操作符:返回一个 NSArray 的对象
(3)数组或set操作符:返回一个 NSArray 或 NSSet 的对象。
具体使用例子和说明如下:

//  Book.h
#import <Foundation/Foundation.h>
#import "Author.h"
@interface Book : NSObject                   //声明Book类代表书
@property (nonatomic, strong) NSMutableArray *authors;  //可变数组,用来存储多个作者
@property (nonatomic, strong) NSArray *authors_2;
@end
//  Book.m
#import "Book.h"
@implementation Book
@end

//  Author.h
#import <Foundation/Foundation.h>
@interface Author : NSObject                 //声明Author类表作者
@property (nonatomic, strong) NSString *name;//属性,表作者的名字
@property (nonatomic, strong) NSNumber *age; //属性,表作者的年龄
@property int year;
@end
//  Author.m
#import "Author.h"
@implementation Author
@end

//  main.m
#import <Foundation/Foundation.h>
#import "Book.h"
#import "Author.h"
int main(int argc, const char * argv[]) 
    @autoreleasepool 
        // 创建对象
        Book *book = [Book new];
        Author *author = [Author new];
        Author *author_2 = [Author new];
        Author *author_3 = [Author new];

        // 对象属性赋值
        author.name = @"wangxiaobo";
        author.age = @20;
        author_2.name = @"guojingming";
        author_2.age = @30;
        author_3.name = @"guojingming";
        author_3.age = @22;

        // 创建一个数组,其元素是Author类的对象
        NSArray *authors_2 = @[author, author_2, author_3];

        // 把数组 authors_2 赋值给 book 的 authors_2 属性
        [book setValue:authors_2 forKey:@"authors_2"];

        // 集合操作符:

        // 1、简单集合操作符

        // (1) @avg:取键路径指定的属性、操作符右边的的值的平均值(用于一个array或者set)
        NSNumber *authorsAveAgeInB = [book valueForKeyPath:@"authors_2.@avg.age"];
        // authorsAveAgeInB == @"24",表示所有作者的年龄平均值为24

        // (2) @count:获得左边键路径的对象个数,省略右边的键路径
        NSNumber *numOfAuthor = [book valueForKeyPath:@"authors_2.@count"];
        // numOfAuthor == @3L,表示有三个作者

        // (3) @max:将键路径指定的属性、操作符右边的的值进行比较,取最大值返回
        NSNumber *oldest = [book valueForKeyPath:@"authors_2.@max.age"];
        // oldest == @30,表示书的作者中年纪最大的是30岁

        // (4) @min:将键路径指定的属性、操作符右边的的值进行比较,取最小值返回
        NSNumber *youngest = [book valueForKeyPath:@"authors_2.@min.age"];
        // youngest == @20,表示书的作者中年纪最小的是20岁

        // (5) @sum:将键路径指定的属性、操作符右边的的值进行求和,返回和的值
        NSNumber *sumAge = [book valueForKeyPath:@"authors_2.@sum.age"];
        // youngest == @50,表示书的所有作者的年龄之和为72岁

        // 2、对象操作符

        // (1) @distinctUnionOfObjects:获得指定属性的所有值组成数组,然后有相同值的只保留一个
        NSArray *aut_name_DU = [book valueForKeyPath:@"authors_2.@distinctUnionOfObjects.name"];
        // aut_name_DU == (
        //      luxun,
        //      guojingming
        // )
        // 即 书的所有作者名有@[@"luxun", @"guojingming", @"guojingming"],后面两个元素相同,删掉一个。

        // (2) @unionOfObjects:获得指定属性的所有值组成数组,即使有相同的也全部保留
        NSArray *aut_name_U = [book valueForKeyPath:@"authors_2.@unionOfObjects.name"];
        // aut_name_U = (
        //      luxun,
        //      guojingming,
        //      guojingming
        // )
        // 后面两个元素相同也都保留

        // 3、Array 和 Set 操作符
        // 用在集合嵌套的情况中,即对象的属性是个集合,集合的元素又是个集合
                     // 再创建两个 Author 的对象,组成第二个数组
        Author *author_4 = [Author new];
        author_4.name = @"luxun";
        Author *author_5 = [Author new];
        author_5.name = @"bingxin";

        // 创建一个数组,其元素是Author类的对象
        NSArray *authors_3 = @[author_4, author_5];
        book.authors = [NSMutableArray array];
        [book.authors addObject:authors_2];
        [book.authors addObject:authors_3];
        // 此时 book.authors 可变数组中有两个元素,每个元素又分别是一个数组

        // (1) @distinctUnionOfArrays:取指定属性中所有的值,若有相同的则只保留一个
        NSArray *aut_name_DU_2 = [book valueForKeyPath:@"authors.@distinctUnionOfArrays.name"];
        // aut_name_DU_2 ==
        // (
        //      bingxin,
        //      luxun,
        //      guojingming
        // )

        // 对比 @distinctUnionOfObjects:只看数组的元素(此处元素是一个数组)
        NSArray *aut_name_DU_2_2 = [book valueForKeyPath:@"authors.@distinctUnionOfObjects.name"];
        // aut_name_DU_2_2 ==
        // (
        //      (
        //      luxun,
        //      guojingming,
        //      guojingming
        //   ),
        //      (
        //      luxun,
        //      bingxin
        //   )
        // )

        // (2) @unionOfArrays:取指定属性中所有的值,若有相同的则只保留一个
        NSArray *aut_name_U_2 = [book valueForKeyPath:@"authors.@unionOfArrays.name"];
        // aut_name_U_2 ==
        // (
        //      luxun,
        //      guojingming,
        //      guojingming,
        //      luxun,
        //      bingxin
        //  )

        // (3) @distinctUnionOfSets:与@distinctUnionOfArray 类似,但是是对 NSSet 嵌套的操作,返回值类型是 NSSet
    
    return 0;

小结:
(1)可见 KVC 的键统一是字符串,而值不能是基本数据类型
(2)相比一般的setter、getter方法,用KVC来访问对象的属性虽然不太简便,但可以动态设置、动态取值、高度规范化的访问,功能也比较强大,可以获取任何对象的任何属性(包括@private的)的值

(本文仍不能涵盖KVC的所有重要用法及其KVO的用法,待续)

以上是关于Objective-C中的KVC的主要内容,如果未能解决你的问题,请参考以下文章

Objective-C KVC 自己主动转换类型研究

Objective-c:为啥在使用 KVC 时私有 ivars 不被外部访问隐藏

Objective-C KVC使用,包你看懂会用

swift中构造方法和Kvc

iOS开发系列--Objective-C 之 KVCKVO

iOS底层学习——KVC