Objective-c KVO and KVC


转载自: http://zhangbin.cc/archives/1839

个人认为这篇文章讲得比较清晰,所以就转了。感谢博主的奉献。


Objective-C Key-Value Coding 和 Key-Value Observing 学习笔记

Key-Value Coding 解决什么问题?

Objective-C 有点像脚本语言, 有很多动态特性. 例如可以从一个字符串生成相应的类:NSClassFromString(@"ClassName"), 将一个字符串转化为消息 (类的方法):NSSelectorFromString(@"setValue:ForKey:") 等等. Key-Value Coding (KVC) 也是类似的一种动态特性, 能够根据字符串直接操作对象的属性, 用起来像操作一个字典一样操作对象, 当然实际作用远大于此.

文档里的例子:

不用 KVC 时的代码是这样:

- (id)tableView:(NSTableView *)tableview
      objectValueForTableColumn:(id)column row:(NSInteger)row 
{
    ChildObject *child = [childrenArray objectAtIndex:row];
    if ([[column identifier] isEqualToString:@"name"]) {
        return [child name];
    }
    if ([[column identifier] isEqualToString:@"age"]) {
        return [child age];
    }
    if ([[column identifier] isEqualToString:@"favoriteColor"]) {
        return [child favoriteColor];
    }
    // And so on.
}

用了 KVC 以后代码可以精简成这样:

- (id)tableView:(NSTableView *)tableview
      objectValueForTableColumn:(id)column row:(NSInteger)row 
{

    ChildObject *child = [childrenArray objectAtIndex:row];
    return [child valueForKey:[column identifier]];
}

使用 KVC 读写对象的属性

读取属性可以使用 [someObject valueForKey:@"<key>"], 前提是对象定义了名为 <key> 或者 _<key>的成员变量, 或者声明了 -<key> 方法. 写入时使用 [someObject setValue:v forKey:@"<key>"], 相应的对象需要声明了 -set<Key>: 方法. 所以, 一般只要把某个属性定义为 @property, 这个属性就符合了 KVC 的要求.

用 -setValuesForKeysWithDictionary: 可以批量更新一批属性的值, 类似 python 中 dict.update()方法.

如果对象中不存在 <key> 这个属性, 那么读取时会调用 someObject 的 -valueForUndefinedKey: 方法, 默认实现会抛出 NSUndefinedKeyException 异常. 写入不存在的属性时会相应的调用 -setValue:forUndefinedKey: 方法, 默认抛出相同的异常.

KVC 的一系列方法是在 Foundation 框架中以 NSObject 的一个 catalog 的形式定义的 (NSKeyValueCoding.h), 所以使用时无需额外声明什么, 所有对象都支持.

KeyPath 是什么

除了 -valueForKey: 和 -setValue:forKey:, 还有 -valueForKeyPath: 和 -setValue:forKeyPath: 方法. KeyPath 的作用是, 假如 someObject 的 child 成员变量也是一个类, child 有一个 int 型的成员变量 val, 那么可以如下设置 val 的值:

[someObject setValue:@2 forKeyPath:@"child.val"];

效果和

someObject.child.val = 2; 

是一样的.

如果 object 是一个字典, 那么还可以通过 KeyPath 访问字典中的键值, 甚至嵌套多层. 例如下面这个类:

@interface Test : NSObject

@property (strong, nonatomic) NSMutableDictionary *dict;

@end

@implementation Test

- (id)init
{
    self = [super init];
    if (self)
    {
        self.dict = [[NSMutableDictionary alloc] init];
    }
    return self;
}

@end

执行下面这些代码:

Test *t = [[Test alloc] init];

[t setValue:@1 forKeyPath:@"dict.a"];
NSLog(@"%@", [t valueForKeyPath:@"dict.a"]);

[t setValue:[[NSMutableDictionary alloc] init] forKeyPath:@"dict.b"];
[t setValue:@2 forKeyPath:@"dict.b.c"];
NSLog(@"%@", [t valueForKeyPath:@"dict.b.c"]);

NSLog(@"%@", [t valueForKeyPath:@"dict"]);

会得到这些输出:

2012-08-15 22:21:26.254 TestKvcKvo[768:c07] 1
2012-08-15 22:21:26.255 TestKvcKvo[768:c07] 2
2012-08-15 22:21:26.255 TestKvcKvo[768:c07] {
    a = 1;
    b =     {
        c = 2;
    };
}

再也不用写嵌套好几层的 [[xx objectForKey:@"yy"] objectForKey:"zz"] 了.

如果属性是数组或集合?

简单的方法是通过 @property 或者 getter/setter 把容器整个暴露出来, 当然更好的实践是提供额外的方法来封装. 如果想用到 KVC 提供的额外特性, 封装方法需要按下面的规范来命名. 基本上每个方法都有 NSArray 或者 NSSet 中的相应方法.

对数组形式的属性:

  • 读取操作:
    • -countOf<Key>: 必须实现;
    • -objectIn<Key>AtIndex: 或者 -<key>AtIndexes: 至少实现一个;
    • 可选的 -get<Key>:range:, 类似 NSArray 的 -getObjects:range:.
  • 写入操作:
    • -insertObject:in<Key>AtIndex: 或 -insert<Key>:atIndexes: 至少实现一个;
    • -removeObjectFrom<Key>AtIndex: 或 -remove<Key>AtIndexes: 至少实现一个;
    • 可选的 -replaceObjectIn<Key>AtIndex:withObject: 或者 -replace<Key>AtIndexes:with<Key>:.

对集合形式的属性:

  • 读取操作:
    • -countOf<Key>: 必须实现;
    • -enumeratorOf<Key>: 必须实现, 对应 NSSet 的方法 -objectEnumerator;
    • -memberOf<Key>: 必须实现, 对应 NSSet 的方法 -member:.
  • 写入操作:
    • -add<Key>Object: 或 -add<Key>: 至少实现一个;
    • -remove<Key>Object: 或 -remove<Key>: 至少实现一个;
    • 可选的 -intersect<Key>:, 对应 NSMutableSet 的 -intersetSet:.

这么做的好处?

  • 可以使用 Key-Value Observing, 后面有详细说明;
  • KeyPath 里面可以使用一些额外的操作. 例如 someObject.items 是一个数组形式的属性, 那么使用 NSNumber *avg = [someObject valueForKeyPath:@"@avg.items"]; 可以得到所有 items 的平均值. 类似的操作还有 @count, @max, @min, @sum, 完整的列表见链接.
  • 注意对象间比较大小的操作是通过 -compare: 方法进行的, 因此只要实现了这个方法, 就可以对自定义对象使用上面这些操作. 用起来有点像 STL, 不过这些这些操作都是定死的, 没法自己扩展.

Key-Value Observing 解决什么问题?

相当于在 Foundation 框架内提供了一个观察者模式的解决方案, 所有满足 KVC 条件的对象都可以使用. 当对象的某个属性发生变化时, 会向相关的观察者发送通知, 告知哪个属性变化了, 从什么值变成什么. 甚至可以在值发生变化之前得到通知.

假如一个类的声明如下:

@interface Test : NSObject
@property (assign, nonatomic) int value;
@end

@implementation Test
@end

另一个对象 (假设是 AppDelegate) 想要观察某个 Test 类实例的 value 属性的变化, 首先需要将自己注册成观察者:

Test *t = [[Test alloc] init];

[t addObserver:self
    forKeyPath:@"value"
       options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
       context:nil
 ];

观察者自己需要实现 - (void)observeValueForKeyPath:ofObject:change:context: 这个方法. 当观察的属性发生变化时, 这个方法会被自动调用:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    NSLog(@"observer: keyPath='%@' change: %@", keyPath, change);
}

那么只要 t.value 发生变化, 例如 t.value = 1; 就会得到这样的输出:

2012-08-16 09:03:44.303 TestKvcKvo[13610:c07] observer: keyPath='value' change: {
    kind = 1;
    new = 1;
    old = 0;
}

对数组或集合进行观察

使用时跟普通属性没什么区别, change 字典同样会告知发生了什么变化, 是添加, 删除还是替换, 受影响的下标是哪些. 例如上面的 Test 类新增了一个 @property (nonatomic, strong) NSMutableArray *arr;, 并按照 KVC 的要求实现了相应的封装方法. 当下面的变化发生时:

[t insertObject:@1 inArrAtIndex:0];
[t insertObject:@2 inArrAtIndex:1];
[t replaceObjectInArrAtIndex:0 withObject:@3];
[t removeObjectFromArrAtIndex:0];
[t.arr insertObject:@100 atIndex:0]; // 不会触发 KVO

会得到以下输出 (观察函数同上):

2012-08-16 09:03:44.300 TestKvcKvo[13610:c07] observer: keyPath='arr' change: {
    indexes = "<NSIndexSet: 0x6e659e0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 2;
    new =     (
        1
    );
}
2012-08-16 09:03:44.301 TestKvcKvo[13610:c07] observer: keyPath='arr' change: {
    indexes = "<NSIndexSet: 0x6a61980>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
    kind = 2;
    new =     (
        2
    );
}
2012-08-16 09:03:44.302 TestKvcKvo[13610:c07] observer: keyPath='arr' change: {
    indexes = "<NSIndexSet: 0x6e659e0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 4;
    new =     (
        3
    );
    old =     (
        1
    );
}
2012-08-16 09:03:44.302 TestKvcKvo[13610:c07] observer: keyPath='arr' change: {
    indexes = "<NSIndexSet: 0x6e659e0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 3;
    old =     (
        3
    );
}

kind 的值可以取以下 4 种:

enum {
   NSKeyValueChangeSetting = 1, // 普通赋值
   NSKeyValueChangeInsertion = 2, // 容器插入
   NSKeyValueChangeRemoval = 3, // 容器删除
   NSKeyValueChangeReplacement = 4 // 容器替换
};
typedef NSUInteger NSKeyValueChange;

当然使用的时候无论是 change 字典中的键名还是键值都必须用常量来检查. 官方文档里还有很多细节, 比如当一个属性是由其他属性计算得到时怎么做 KVO, 以及内部的实现原理等等.


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ava实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),可运行高分资源 Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现
C语言是一种广泛使用的编程语言,它具有高效、灵活、可移植性强等特点,被广泛应用于操作系统、嵌入式系统、数据库、编译器等领域的开发。C语言的基本语法包括变量、数据类型、运算符、控制结构(如if语句、循环语句等)、函数、指针等。下面详细介绍C语言的基本概念和语法。 1. 变量和数据类型 在C语言中,变量用于存储数据,数据类型用于定义变量的类型和范围。C语言支持多种数据类型,包括基本数据类型(如int、float、char等)和复合数据类型(如结构体、联合等)。 2. 运算符 C语言中常用的运算符包括算术运算符(如+、、、/等)、关系运算符(如==、!=、、=、<、<=等)、逻辑运算符(如&&、||、!等)。此外,还有位运算符(如&、|、^等)和指针运算符(如、等)。 3. 控制结构 C语言中常用的控制结构包括if语句、循环语句(如for、while等)和switch语句。通过这些控制结构,可以实现程序的分支、循环和多路选择等功能。 4. 函数 函数是C语言中用于封装代码的单元,可以实现代码的复用和模块化。C语言中定义函数使用关键字“void”或返回值类型(如int、float等),并通过“{”和“}”括起来的代码块来实现函数的功能。 5. 指针 指针是C语言中用于存储变量地址的变量。通过指针,可以实现对内存的间接访问和修改。C语言中定义指针使用星号()符号,指向数组、字符串和结构体等数据结构时,还需要注意数组名和字符串常量的特殊性质。 6. 数组和字符串 数组是C语言中用于存储同类型数据的结构,可以通过索引访问和修改数组中的元素。字符串是C语言中用于存储文本数据的特殊类型,通常以字符串常量的形式出现,用双引号("...")括起来,末尾自动添加'\0'字符。 7. 结构体和联合 结构体和联合是C语言中用于存储不同类型数据的复合数据类型。结构体由多个成员组成,每个成员可以是不同的数据类型;联合由多个变量组成,它们共用同一块内存空间。通过结构体和联合,可以实现数据的封装和抽象。 8. 文件操作 C语言中通过文件操作函数(如fopen、fclose、fread、fwrite等)实现对文件的读写操作。文件操作函数通常返回文件指针,用于表示打开的文件。通过文件指针,可以进行文件的定位、读写等操作。 总之,C语言是一种功能强大、灵活高效的编程语言,广泛应用于各种领域。掌握C语言的基本语法和数据结构,可以为编程学习和实践打下坚实的基础。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值