动手实现 KVC 键值编码

Mike Ash Friday Q&A 中文译文:动手实现 KVC 键值编码

作者 TommyWu
封面圖片: 动手实现 KVC 键值编码

译文 · 原文: Friday Q&A 2013-02-08: Let's Build Key-Value Coding · 作者 Mike Ash

原文:https://www.mikeash.com/pyblog/friday-qa-2013-02-08-lets-build-key-value-coding.html 发布:2013-02-08 作者:Mike Ash 译者:MiMo(mimo-v2.5-pro);代码块保留英文原样


上次,我展示了如何构建 NSObject 的基本功能。但我略过了键值编码(KVC),因为 valueForKey:setValue:forKey: 的实现足够复杂,需要专门撰写一篇文章。本文就是这个内容。

基础
键值编码(KVC)是一个允许通过字符串访问对象属性的 API。NSObject 实现了相关方法,用于根据键名查找访问器方法或实例变量,并通过它们获取或设置值。

KVC 的基础由两个基本方法构成。

valueForKey: 方法会搜索与键同名的 getter 方法。如果找到,就调用该方法并返回其返回值。如果没有找到,则搜索与键同名的实例变量。如果仍然没有找到,它会寻找一个带下划线前缀的、与键同名的实例变量。如果找到实例变量,则返回其当前持有的值。

setValue:forKey: 方法执行相同的搜索逻辑,不过它搜索的是 setter 方法而非 getter。然后,它要么调用 setter 方法,要么直接设置实例变量的值。

这两个方法的一个有趣特性是:它们处理原始值时,会通过自动装箱(boxing)和拆箱(unboxing)将其转换为 NSNumber 或 NSValue 的实例。你可以使用 valueForKey: 调用一个返回 int 类型的方法,其结果将是一个包含返回值的 NSNumber 对象。同样,你可以使用 setValue:forKey: 调用一个接受 int 类型参数的方法,传入一个 NSNumber 对象,它会自动提取其中的整数值。

KVC 还有键路径(key paths)的概念,即用点号连接起来的一系列键,例如:

foo.bar.baz

要处理键路径(key path),有对应的方法:valueForKeyPath:setValue:forKeyPath:。这些方法会递归地调用更基础的方法。

KVC(键值编码)中还有许多其他用于管理集合的功能,但这些不太有趣,这里就略过不谈了。

代码 今天的代码可在 GitHub 上的 MAObject 项目中找到:

https://github.com/mikeash/MAObject

让我们开始吧。

valueForKey: 方法首先会检查是否存在一个与键同名的 getter 方法。

- (id)valueForKey: (NSString *)key
{
SEL getterSEL = NSSelectorFromString(key);
if([self respondsToSelector: getterSEL])
{

如果对象响应该选择子(selector),它将使用访问器来获取值。具体如何实现取决于访问器的返回类型。为了做好准备,系统会获取该方法的返回类型和 IMP(方法实现):

NSMethodSignature *sig = [self methodSignatureForSelector: getterSEL];
char type = [sig methodReturnType][0];
IMP imp = [self methodForSelector: getterSEL];

如果返回类型是对象或类,那么代码就很简单:将 IMP(Implementation,方法实现)强制转换为正确的函数指针类型,调用它,并返回其返回值:

if(type == @encode(id)[0] || type == @encode(Class)[0])
{
return ((id (*)(id, SEL))imp)(self, getterSEL);
}

否则,该方法会返回一个原始类型(primitive),这正是事情变得有趣的地方。

没有便捷的方法可以获取一个带有任意类型的函数指针(function pointer)、调用它,并将结果装箱(box up)。我们不得不采用暴力方式,逐一列举所有可能性,并为每种可能的类型编写处理代码。我创建了一个小宏(macro)来辅助这个过程:

else
{
#define CASE(ctype, selectorpart) \
if(type == @encode(ctype)[0]) \
return [NSNumber numberWith ## selectorpart: ((ctype (*)(id, SEL))imp)(self, getterSEL)];

核心思想是为每种类型分配一行代码。你将类型名称作为一个参数传递,另一个参数是与 [NSNumber numberWithType:] 匹配的选择子部分。该宏利用这些参数构造代码,用于检查类型是否匹配,若匹配则使用正确的函数指针类型调用 IMP(方法实现)。有了这个宏,只需列出所有支持的原始类型即可:

CASE(char, Char);
CASE(unsigned char, UnsignedChar);
CASE(short, Short);
CASE(unsigned short, UnsignedShort);
CASE(int, Int);
CASE(unsigned int, UnsignedInt);
CASE(long, Long);
CASE(unsigned long, UnsignedLong);
CASE(long long, LongLong);
CASE(unsigned long long, UnsignedLongLong);
CASE(float, Float);
CASE(double, Double);

别忘了取消定义 CASE 宏,以便之后能复用该名称:

#undef CASE

如果找到了匹配的 case,方法会立即返回。如果执行到这里方法仍在运行,就说明类型未知。与其设法优雅地处理这种情况,该方法直接抛出异常来报错:

[NSException raise: NSInternalInconsistencyException format: @"Class %@ key %@ don't know how to interpret method return type from getter, signature is %@", [isa description], key, sig];
}
}

以上是处理存在 getter 方法情况的代码。若不存在 getter 方法,KVC 会回退到实例变量(instance variables)。首先,它会尝试获取与 key 同名的实例变量:

Ivar ivar = class_getInstanceVariable(isa, [key UTF8String]);

如果失败,它会再次尝试,这次会添加一个前导下划线:

if(!ivar)
ivar = class_getInstanceVariable(isa, [[@"_" stringByAppendingString: key] UTF8String]);

如果其中任何一个发现了 instance variable(实例变量),它就会继续实际获取其值。为了获取变量的内容,我们需要知道它存储的位置。这是通过获取变量的偏移量,并将其加到 self 的值上来完成的:

if(ivar)
{
ptrdiff_t offset = ivar_getOffset(ivar);
char *ptr = (char *)self;
ptr += offset;

首先将 self 强制转换为 char * 类型,因为偏移量是以字节为单位的,对 char * 进行操作能确保 += 运算符合预期。

我们还需要知道变量的类型:

const char *type = ivar_getTypeEncoding(ivar);

如果类型是对象或类,那么它会直接提取该值并返回:

const char *type = ivar_getTypeEncoding(ivar);
if(type[0] == @encode(id)[0] || type[0] == @encode(Class)[0])
{
return *(id *)ptr;
}

否则,它再次回退到特殊情况的处理。这段代码使用了一个稍微不同的 CASE 宏。这个宏会检查类型,如果匹配,则从 ptr 中提取出值:

else
{
#define CASE(ctype, selectorpart) \
if(strcmp(type, @encode(ctype)) == 0) \
return [NSNumber numberWith ## selectorpart: *(ctype *)ptr];

再次,这里有一个很长的支持类型列表:

CASE(char, Char);
CASE(unsigned char, UnsignedChar);
CASE(short, Short);
CASE(unsigned short, UnsignedShort);
CASE(int, Int);
CASE(unsigned int, UnsignedInt);
CASE(long, Long);
CASE(unsigned long, UnsignedLong);
CASE(long long, LongLong);
CASE(unsigned long long, UnsignedLongLong);
CASE(float, Float);
CASE(double, Double);

随后进行宏清理:

#undef CASE

如果没有任何匹配项,这段代码会回退到使用 ptr 的内容创建一个通用的 NSValue。由于数据已经在内存中布局好,进行这种回退处理非常简单,而不会像上面的 getter 代码那样直接抛出异常:

return [NSValue valueWithBytes: ptr objCType: type];
}
}

最后,如果没有找到对应的 getter 方法或实例变量,该方法会抛出一个异常。末尾的占位 return 语句仅用于确保编译器不会因函数未返回值而产生警告:

[NSException raise: NSInternalInconsistencyException format: @"Class %@ is not key-value compliant for key %@", [isa description], key];
return nil;
}

setValue:forKey: 方法的处理方式类似,但由于它需要设置而非检索值,存在一些差异。

它首先构造出要查找的 setter 方法名称。valueForKey: 可以直接将键转换为选择子(selector),但此方法需要额外步骤。生成 setter 方法的方式是:将键的首字母大写,然后在开头加上 “set”,结尾加上冒号:

- (void)setValue: (id)value forKey: (NSString *)key
{
NSString *setterName = [NSString stringWithFormat: @"set%@:", [key capitalizedString]];

然后将其转换为一个选择子(selector),并检查该对象是否能响应:

SEL setterSEL = NSSelectorFromString(setterName);
if([self respondsToSelector: setterSEL])
{

如果满足条件,它会获取该方法的参数类型和 IMP(方法实现指针),逻辑与上面的 getter 代码类似:

NSMethodSignature *sig = [self methodSignatureForSelector: setterSEL];
char type = [sig getArgumentTypeAtIndex: 2][0];
IMP imp = [self methodForSelector: setterSEL];

如果类型是对象或类,它直接调用设置器,传入 value,然后返回:

if(type == @encode(id)[0] || type == @encode(Class)[0])
{
((void (*)(id, SEL, id))imp)(self, setterSEL, value);
return;
}

又到了使用 CASE 宏的时候了。当匹配成功时,这个宏会调用 IMP(方法实现指针),并将 [value typeValue] 作为参数传递过去:

else
{
#define CASE(ctype, selectorpart) \
if(type == @encode(ctype)[0]) { \
((void (*)(id, SEL, ctype))imp)(self, setterSEL, [value selectorpart ## Value]); \
return; \
}

以下是所有情况的完整列表:

CASE(char, char);
CASE(unsigned char, unsignedChar);
CASE(short, short);
CASE(unsigned short, unsignedShort);
CASE(int, int);
CASE(unsigned int, unsignedInt);
CASE(long, long);
CASE(unsigned long, unsignedLong);
CASE(long long, longLong);
CASE(unsigned long long, unsignedLongLong);
CASE(float, float);
CASE(double, double);

随后进行宏清理:

#undef CASE

最后,若类型未知,则会抛出异常(exception):

[NSException raise: NSInternalInconsistencyException format: @"Class %@ key %@ set from incompatible object %@", [isa description], key, value];
}
}

如果没有找到 setter 方法,则会搜索实例变量(instance variables)。这个过程不需要字符串操作,因为实例变量的名称不会像 setter 方法的名称那样发生变化。这段代码执行相同的检查,查看是否有一个带前导下划线的实例变量:

Ivar ivar = class_getInstanceVariable(isa, [key UTF8String]);
if(!ivar)
ivar = class_getInstanceVariable(isa, [[@"_" stringByAppendingString: key] UTF8String]);

如果该实例变量存在,它就会创建一个指向该变量的指针,并获取其类型,这与 valueForKey: 的处理方式一致:

if(ivar)
{
ptrdiff_t offset = ivar_getOffset(ivar);
char *ptr = (char *)self;
ptr += offset;
const char *type = ivar_getTypeEncoding(ivar);

如果变量是一个「object(对象)」或「class pointer(类指针)」,代码可以直接设置它。嗯,几乎是直接地。为了确保内存管理正确,需要进行一些微小的「retain(保留)」和「release(释放)」操作(译注:指手动引用计数,现代系统可能已使用自动引用计数):

if(type[0] == @encode(id)[0] || type[0] == @encode(Class)[0])
{
value = [value retain];
[*(id *)ptr release];
*(id *)ptr = value;
return;
}

否则,值会被装箱(boxed),此时需要提取原始值。如果该值是一个 NSValue,且其类型与实例变量完全相同,就可以直接使用 getValue: 方法将值直接复制过来:

else if(strcmp([value objCType], type) == 0)
{
[value getValue: ptr];
return;
}

如果这招不奏效,就该退而求其次,使用最后那一大串长列表了。这个版本的 CASE 宏会将 ptr 处的值(经过适当类型转换)设置为 [value typeValue]

else
{
#define CASE(ctype, selectorpart) \
if(strcmp(type, @encode(ctype)) == 0) { \
*(ctype *)ptr = [value selectorpart ## Value]; \
return; \
}

对于基本类型(primitive types)的传统穷举方式如下:

CASE(char, char);
CASE(unsigned char, unsignedChar);
CASE(short, short);
CASE(unsigned short, unsignedShort);
CASE(int, int);
CASE(unsigned int, unsignedInt);
CASE(long, long);
CASE(unsigned long, unsignedLong);
CASE(long long, longLong);
CASE(unsigned long long, unsignedLongLong);
CASE(float, float);
CASE(double, double);

宏清理:

#undef CASE

最后,如果所有条件分支均未命中,则抛出异常:

[NSException raise: NSInternalInconsistencyException format: @"Class %@ key %@ set from incompatible object %@", [isa description], key, value];
}
}

若既未找到 setter 方法也未找到实例变量,则抛出异常以示错误:

[NSException raise: NSInternalInconsistencyException format: @"Class %@ is not key-value compliant for key %@", [isa description], key];
}

键路径

为了完善 KVC 的实现,我还将实现 valueForKeyPath:setValue:forKeyPath:

valueForKeyPath: 首先要做的事情是查找键路径中的 . 符号。如果不存在,则将其视为一个普通的键(key),并传递给 valueForKey:

- (id)valueForKeyPath: (NSString *)keyPath
{
NSRange range = [keyPath rangeOfString: @"."];
if(range.location == NSNotFound)
return [self valueForKey: keyPath];

否则,键会被拆分为两部分。点号(”.”)之前的部分是本地键(local key),后面的部分则是键路径的剩余部分:

NSString *key = [keyPath substringToIndex: range.location];
NSString *rest = [keyPath substringFromIndex: NSMaxRange(range)];

键被传递给valueForKey:方法。

id next = [self valueForKey: key];

接着,valueForKeyPath: 会递归地发送给下一个对象。

return [next valueForKeyPath: rest];
}

它的实现会进一步分解剩余部分,直到每个”.” 都被消耗掉。结果是一系列 valueForKey:(键值访问方法)调用的链,返回最后一次调用的结果。

setValue:forKeyPath:(键路径赋值方法)的工作方式类似。如果在键路径(key path)中没有”.”,则调用 setValue:forKey:(键值赋值方法)并返回:

- (void)setValue: (id)value forKeyPath: (NSString *)keyPath
{
NSRange range = [keyPath rangeOfString: @"."];
if(range.location == NSNotFound)
{
[self setValue: value forKey: keyPath];
return;
}

否则,提取键和剩余部分:

NSString *key = [keyPath substringToIndex: range.location];
NSString *rest = [keyPath substringFromIndex: NSMaxRange(range)];

使用 valueForKey: 获取下一个对象。

id next = [self valueForKey: key];

然后递归地向 next 发送 setValue:forKeyPath: 消息,并将剩余部分作为键路径(key path)传递:

[next setValue: value forKeyPath: rest];
}

其结果是一连串 valueForKey: 调用,最终在链条的最后一个对象上调用 setValue:forKey:

结论

你现在可以看到键值编码(Key-Value Coding)内部是如何工作的了。并没有什么特别复杂之处。它主要就是一长串待尝试操作的列表。Cocoa 的实现更智能一些,能够利用像 NSInvocation 这样的机制来实现更全面的覆盖,但基本思路就是如此。NSInvocation 的相当一部分内容,就是内嵌了对所有需要处理的不同情况的知识。

今天就到这里。愿你平静地编码你的键与值。下次再见,因为 Friday Q & A 是由读者建议驱动的,请发送你的主题想法!


#Original (English)

Source: https://www.mikeash.com/pyblog/friday-qa-2013-02-08-lets-build-key-value-coding.html

Last time, I showed how to build the basic functionality of NSObject. I left out key-value coding, because the implementation of valueForKey: and setValue:forKey: is complex enough to need its own article. This is that article.

BasicsKey-value coding (KVC) is an API that allows string-based access to object properties. NSObject implements the methods to look up accessor methods or instance variables based on the key name, and fetch or set the value using those.

There are two basic methods that form the basis of KVC.

The valueForKey: method searches for a getter method with the same name as the key. If found, it calls the method and returns its return value. If none is found, it searches for an instance variable with the same name as the key. Failing those, it looks for an instance variable with the same name as the key, but prefixed with an underscore. If an instance variable is found, it returns the value it currently holds.

The setValue:forKey: method performs the same search, except that it searches for a setter method rather than a getter. It then either calls the setter or sets the instance variable directly.

An interesting feature of both of these methods is that they work with primitive values by automatically boxing and unboxing them into instances of NSNumber or NSValue. You can use valueForKey: to invoke a method that returns int, and the result will be an NSNumber object containing the return value. Likewise, you can use setValue:forKey: to invoke a method that takes int, pass it an NSNumber, and it will automatically extract the integer value.

KVC also has the concept of key paths, which are sequences of keys put together with periods, like:

foo.bar.baz

There are corresponding methods to work with key paths: valueForKeyPath: and setValue:forKeyPath:. These simply call the more primitive methods recursively.

There are a bunch of other KVC features for managing collections, but these are less interesting and I’m going to skip over them here.

CodeToday’s code is available on GitHub as part of the MAObject project:

https://github.com/mikeash/MAObject

Let’s get to it.

valueForKey

first thing that valueForKey: does is check for a getter method with the same name as the key.

- (id)valueForKey: (NSString *)key
{
SEL getterSEL = NSSelectorFromString(key);
if([self respondsToSelector: getterSEL])
{

If the object responds to that selector, it will use the accessor to get the value. Exactly how that’s done will depend on the accessor’s return type. To get ready, it fetches the return type and IMP for the method:

NSMethodSignature *sig = [self methodSignatureForSelector: getterSEL];
char type = [sig methodReturnType][0];
IMP imp = [self methodForSelector: getterSEL];

If the return type is an object or a class, then the code is simple: cast the IMP to the right function pointer type, call it, and return what it returns:

if(type == @encode(id)[0] || type == @encode(Class)[0])
{
return ((id (*)(id, SEL))imp)(self, getterSEL);
}

Otherwise, the method returns a primitive, which is where things get interesting.

There is no convenient way to take a function pointer with an arbitrary type, call it, and box up the result. We have to do things the brute-force way, by enumerating all of the possibilities one by one and writing code to handle each possible type. I created a small macro to help with this:

else
{
#define CASE(ctype, selectorpart) \
if(type == @encode(ctype)[0]) \
return [NSNumber numberWith ## selectorpart: ((ctype (*)(id, SEL))imp)(self, getterSEL)];

The idea is that each type gets a single line. You pass the type name as one parameter, and a selector part that fits in with [NSNumber numberWithType:] as the other parameter. The macro uses these to construct code that checks for the type and calls the IMP with the right function pointer type if it matches. With this macro, it’s just a matter of writing out every supported primitive type:

CASE(char, Char);
CASE(unsigned char, UnsignedChar);
CASE(short, Short);
CASE(unsigned short, UnsignedShort);
CASE(int, Int);
CASE(unsigned int, UnsignedInt);
CASE(long, Long);
CASE(unsigned long, UnsignedLong);
CASE(long long, LongLong);
CASE(unsigned long long, UnsignedLongLong);
CASE(float, Float);
CASE(double, Double);

Let’s not forget to undefine the CASE macro so we can reuse the name later:

#undef CASE

If a matching case was found, then the method returned immediately. If the method is still running at this point, then the type isn’t known. Rather than try to handle this gracefully somehow, the method just throws an exception to complain:

[NSException raise: NSInternalInconsistencyException format: @"Class %@ key %@ don't know how to interpret method return type from getter, signature is %@", [isa description], key, sig];
}
}

That was the code to handle the case where a getter method exists. If no getter exists, then KVC falls back to instance variables. First, it tries to get an instance variable with the same name as the key:

Ivar ivar = class_getInstanceVariable(isa, [key UTF8String]);

If that fails, it tries again with a leading underscore:

if(!ivar)
ivar = class_getInstanceVariable(isa, [[@"_" stringByAppendingString: key] UTF8String]);

If either of those found an instance variable, it proceeds to actually fetching its value. In order to fetch the contents of the variable, we need to know where it’s stored. This is done by getting the variable’s offset, and adding it to the value of self:

if(ivar)
{
ptrdiff_t offset = ivar_getOffset(ivar);
char *ptr = (char *)self;
ptr += offset;

self is cast to char * first, because the offset is in bytes, and operating on a char * ensures that the += operation does what we need.

We also need to know the type of the variable:

const char *type = ivar_getTypeEncoding(ivar);

If the type is an object or class, then it just extracts the value directly and returns it:

const char *type = ivar_getTypeEncoding(ivar);
if(type[0] == @encode(id)[0] || type[0] == @encode(Class)[0])
{
return *(id *)ptr;
}

Otherwise, it falls back to special cases again. This code uses a slightly different CASE macro. This one checks the type and then extracts the value from ptr if there’s a match:

else
{
#define CASE(ctype, selectorpart) \
if(strcmp(type, @encode(ctype)) == 0) \
return [NSNumber numberWith ## selectorpart: *(ctype *)ptr];

Once again, there’s a long list of supported types:

CASE(char, Char);
CASE(unsigned char, UnsignedChar);
CASE(short, Short);
CASE(unsigned short, UnsignedShort);
CASE(int, Int);
CASE(unsigned int, UnsignedInt);
CASE(long, Long);
CASE(unsigned long, UnsignedLong);
CASE(long long, LongLong);
CASE(unsigned long long, UnsignedLongLong);
CASE(float, Float);
CASE(double, Double);

Followed by macro cleanup:

#undef CASE

This code falls back to creating a generic NSValue with the contents of ptr if there’s no match. Because the data is already laid out in memory, it’s trivial to have a fallback here, rather than just throwing an exception like the getter code above does:

return [NSValue valueWithBytes: ptr objCType: type];
}
}

Finally, if no getter or instance variable was found, the method throws an exception. The dummy return statement at the end is just to ensure that the compiler doesn’t complain about not returning a value:

[NSException raise: NSInternalInconsistencyException format: @"Class %@ is not key-value compliant for key %@", [isa description], key];
return nil;
}

That takes care of valueForKey:.

setValue:forKey

setValue:forKey: method works similarly, but there are some differences due to the fact that it has to set values rather than retrieve them.

The first thing it does is construct the name of the setter method to search for. valueForKey: can simply translate the key directly to a selector, but this method needs to do a bit of work. The setter method is generated by capitalizing the first letter of the key, then adding “set” to the beginning, and a colon at the end:

- (void)setValue: (id)value forKey: (NSString *)key
{
NSString *setterName = [NSString stringWithFormat: @"set%@:", [key capitalizedString]];

It then turns that into a selector and checks to see if the object responds:

SEL setterSEL = NSSelectorFromString(setterName);
if([self respondsToSelector: setterSEL])
{

If it does, it fetches the method’s argument type and IMP much like the getter code above:

NSMethodSignature *sig = [self methodSignatureForSelector: setterSEL];
char type = [sig getArgumentTypeAtIndex: 2][0];
IMP imp = [self methodForSelector: setterSEL];

If the type is an object or class, it simply calls the setter, passing value, and returns:

if(type == @encode(id)[0] || type == @encode(Class)[0])
{
((void (*)(id, SEL, id))imp)(self, setterSEL, value);
return;
}

Otherwise, it’s once again time for a CASE macro. This one calls the IMP, passing [value typeValue] as the parameter, when a match is found:

else
{
#define CASE(ctype, selectorpart) \
if(type == @encode(ctype)[0]) { \
((void (*)(id, SEL, ctype))imp)(self, setterSEL, [value selectorpart ## Value]); \
return; \
}

Here is the big list of cases:

CASE(char, char);
CASE(unsigned char, unsignedChar);
CASE(short, short);
CASE(unsigned short, unsignedShort);
CASE(int, int);
CASE(unsigned int, unsignedInt);
CASE(long, long);
CASE(unsigned long, unsignedLong);
CASE(long long, longLong);
CASE(unsigned long long, unsignedLongLong);
CASE(float, float);
CASE(double, double);

Followed by macro cleanup:

#undef CASE

Last, if the type is unknown, it throws an exception:

[NSException raise: NSInternalInconsistencyException format: @"Class %@ key %@ set from incompatible object %@", [isa description], key, value];
}
}

If no setter method is found, then it searches for instance variables. No string manipulation is needed, since the instance variable’s name doesn’t change the way the setter’s name does. This code does the same check for instance variables with a leading underscore:

Ivar ivar = class_getInstanceVariable(isa, [key UTF8String]);
if(!ivar)
ivar = class_getInstanceVariable(isa, [[@"_" stringByAppendingString: key] UTF8String]);

If the instance variable exists, it creates a pointer to it and gets its type just like valueForKey: does:

if(ivar)
{
ptrdiff_t offset = ivar_getOffset(ivar);
char *ptr = (char *)self;
ptr += offset;
const char *type = ivar_getTypeEncoding(ivar);

If the variable is an object or class pointer, the code can set it directly. Well, nearly directly. There’s a minor retain release dance to be done in order to ensure that memory management is correct:

if(type[0] == @encode(id)[0] || type[0] == @encode(Class)[0])
{
value = [value retain];
[*(id *)ptr release];
*(id *)ptr = value;
return;
}

Otherwise, value is boxed, and the primitive value needs to be extracted. If value is an NSValue with the exact same type as the instance variable, the getValue: method can be used to simply copy the value over directly:

else if(strcmp([value objCType], type) == 0)
{
[value getValue: ptr];
return;
}

If that doesn’t work, it’s time to fall back to the last long list of cases. This version of the CASE macro sets the value at ptr, appropriately cast, to [value typeValue]:

else
{
#define CASE(ctype, selectorpart) \
if(strcmp(type, @encode(ctype)) == 0) { \
*(ctype *)ptr = [value selectorpart ## Value]; \
return; \
}

The traditional exhaustive enumeration of primitive types follows:

CASE(char, char);
CASE(unsigned char, unsignedChar);
CASE(short, short);
CASE(unsigned short, unsignedShort);
CASE(int, int);
CASE(unsigned int, unsignedInt);
CASE(long, long);
CASE(unsigned long, unsignedLong);
CASE(long long, longLong);
CASE(unsigned long long, unsignedLongLong);
CASE(float, float);
CASE(double, double);

Macro cleanup:

#undef CASE

Finally, if none of the cases were hit, throw an exception:

[NSException raise: NSInternalInconsistencyException format: @"Class %@ key %@ set from incompatible object %@", [isa description], key, value];
}
}

If neither setter method nor instance variable was found, throw an exception to complain:

[NSException raise: NSInternalInconsistencyException format: @"Class %@ is not key-value compliant for key %@", [isa description], key];
}

Key PathsTo round out the implementation of KVC, I’ll implement valueForKeyPath: and setValue:forKeyPath: as well.

The first thing that valueForKeyPath: does is look for a . in the key path. If it doesn’t exist, then it’s treated as a plain key and passed to valueForKey:

- (id)valueForKeyPath: (NSString *)keyPath
{
NSRange range = [keyPath rangeOfString: @"."];
if(range.location == NSNotFound)
return [self valueForKey: keyPath];

Otherwise, the key is split into two pieces. The piece up to the . is the local key, and the following piece is the remainder of the key path:

NSString *key = [keyPath substringToIndex: range.location];
NSString *rest = [keyPath substringFromIndex: NSMaxRange(range)];

The key is passed to valueForKey:

id next = [self valueForKey: key];

Then valueForKeyPath: is sent recursively to the next object:

return [next valueForKeyPath: rest];
}

Its implementation will decompose rest further until every . is consumed. The result is a chain of valueForKey: calls, returning the result of the very last call.

setValue:forKeyPath: works similarly. If there’s no . in the key path, call setValue:forKey: and return:

- (void)setValue: (id)value forKeyPath: (NSString *)keyPath
{
NSRange range = [keyPath rangeOfString: @"."];
if(range.location == NSNotFound)
{
[self setValue: value forKey: keyPath];
return;
}

Otherwise, extract the key and remainder:

NSString *key = [keyPath substringToIndex: range.location];
NSString *rest = [keyPath substringFromIndex: NSMaxRange(range)];

Grab the next object using valueForKey:

id next = [self valueForKey: key];

Then recursively send setValue:forKeyPath: to next, passing rest as the key path:

[next setValue: value forKeyPath: rest];
}

The result is a chain of valueForKey: calls, culminating in a call to setValue:forKey: on the last object in the chain.

ConclusionYou can now see how key-value coding works on the inside. There isn’t anything particularly complicated. It’s largely just a long list of different things to try. Cocoa’s implementation is a bit smarter, and can leverage things like NSInvocation for more comprehensive coverage, but that’s the basic idea. A large part of NSInvocation is simply baked-in knowledge of all the different cases that need to be handled as well.

That’s it for today. May you code your keys and values in peace. Until next time, since Friday Q&A is driven by reader suggestions, please send in your ideas for topics!