从 C++ 到 Objective-C(20):隐式代码

本章中心是两个能够让代码更简洁的特性。它们的目的截然不同:键值对编码可以通过选择第一个符合条件的实现而解决间接方法调用;属性则可以让编译器帮我们生成部分代码。键值对编码实际上是 Cocoa 引入的,而属性则是 Objective-C 2.0 语言新增加的。

键值对编码(KVC)

原则

键值对编码意思是,能够通过数据成员的名字来访问到它的值。这种语法很类似于关联数组(在 Cocoa 中就是NSDictionary),数据成员的名字就是这里的键。NSObject有一个valueForKey:setValue:forKey:方法。如果数据成员就是对象自己,寻值过程就会向下深入下去,此时,这个键应该是一个路径,使用点号 . 分割,对应的方法是valueForKeyPath:setValue:forKeyPath:

@interface A {
    NSString* foo;
}
... // 其它代码
@end

@interface B {
    NSString* bar;
    A* myA;
}
... // 其它代码
@end

@implementation B
...
// 假设 A 类型的对象 a,B 类型的对象 b
A* a = ...;
B* b = ...;
NSString* s1 = [a valueForKey:@"foo"]; // 正确
NSString* s2 = [b valueForKey:@"bar"]; // 正确
NSString* s3 = [b valueForKey:@"myA"]; // 正确
NSString* s4 = [b valueForKeyPath:@"myA.foo"]; // 正确
NSString* s5 = [b valueForKey:@"myA.foo"]; // 错误
NSString* s6 = [b valueForKeyPath:@"bar"]; // 正确
...
@end

这种语法能够让我们对不同的类使用相同的代码来处理同名数据。注意,这里的数据成员的名字都是使用的字符串的形式。这种使用方法的最好的用处在于将数据(名字)绑定到一些触发器(尤其是方法调用)上,例如键值对观察(Key-Value Observing, KVO)等。

拦截

通过valueForKey:或者setValue:forKey:访问数据不是原子操作。这个操作本质上还是一个方法调用。事实上,这种访问当某些方式实现的情况下才是可用的,例如使用属性自动添加的代码等等,或者显式允许直接访问数据。

Apple 的文档对valueForKey:setValue:forKey:的使用有清晰的文档:

对于valueForKey:@"foo"的调用:

  • 如果有方法名为getFoo,则调用getFoo
  • 否则,如果有方法名为foo,则调用foo(这是对常见的情况);
  • 否则,如果有方法名为isFoo,则调用isFoo(主要是布尔值的时候);
  • 否则,如果类的accessInstanceVariablesDirectly方法返回YES,则尝试访问_foo数据成员(如果有的话),否则寻找_isFoo,然后是foo,然后是isFoo
  • 如果前一个步骤成功,则返回对应的值;
  • 如果失败,则调用valueForUndefinedKey:,这个方法的默认实现是抛出一个异常。

对于forKey:@"foo"的调用:

  • 如果有方法名为setFoo:,则调用setFoo:
  • 否则,如果类的accessInstanceVariablesDirectly返回YES,则尝试直接写入数据成员_foo(如果存在的话),否则寻找_isFoo,然后是foo,然后是isFoo
  • 如果失败,则调用setValue:forUndefinedKey:,其默认实现是抛出一个异常。

注意valueForKey:setValue:forKey:的调用可以用于触发任何相关方法。如果没有这个名字的数据成员,则就是一个虚假的调用。例如, 在字符串变量上调用valueForKey:@"length"等价于直接调用length方法,因为这是 KVC 能够找到的第一个匹配。但是,KVC 的性能不如直接调用方法,所以应当尽量避免。

原型

使用 KVC 有一定的方法原型的要求:getters 不能有参数,并且要返回一个对象;setters 需要有一个对象作为参数,不能有返回值。参数的类型不是很重要的,因为你可以使用id作为参数类型。注意,struct 和原生类型(int,float 等)都是支持的:Objective-C 有一个自动装箱机制,可以将这些原生类型封装成NSNumber或者NSValue对象。因此,valueForKey:返回值都是一个对象。如果需要向setValue:forKey:传入nil,需要使用setNilValueForKey:

高级特性

有几点细节需要注意,尽管在这里并不会很详细地讨论这个问题:

  1. keypath 可以包含计算值,例如求和、求平均、最大值、最小值等;使用 @ 标记;
  2. 注意方法一致性,例如valueForKey:或者setValue:forKey:以及关联数组集合中常见的objectForKey:setObject:forKey:。这里,同样使用 @ 进行区分。

Leave a Reply