Last time, I showed how to build the basic functionality ofNSObject. I left out key-value coding, because the implementation ofvalueForKey: andsetValue:forKey: is complex enough to need its own article. This is that article.
Basics
Key-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.
ThevalueForKey: 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.
ThesetValue: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 ofNSNumber orNSValue. You can usevalueForKey: to invoke a method that returnsint, and the result will be anNSNumber object containing the return value. Likewise, you can usesetValue:forKey: to invoke a method that takesint, pass it anNSNumber, 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: andsetValue: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.
Code
Today's code is available on GitHub as part of theMAObject project:
https://github.com/mikeash/MAObject
Let's get to it.
valueForKey:
The first thing thatvalueForKey: does is check for a getter method with the same name as the key.
-(id)valueForKey:(NSString*)key{SELgetterSEL=NSSelectorFromString(key);if([selfrespondsToSelector: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 andIMP for the method:
NSMethodSignature*sig=[selfmethodSignatureForSelector:getterSEL];chartype=[sigmethodReturnType][0];IMPimp=[selfmethodForSelector:getterSEL];
If the return type is an object or a class, then the code is simple: cast theIMP 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{#defineCASE(ctype,selectorpart) \if(type==@encode(ctype)[0]) \return[NSNumbernumberWith##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 theIMP 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(unsignedchar,UnsignedChar);CASE(short,Short);CASE(unsignedshort,UnsignedShort);CASE(int,Int);CASE(unsignedint,UnsignedInt);CASE(long,Long);CASE(unsignedlong,UnsignedLong);CASE(longlong,LongLong);CASE(unsignedlonglong,UnsignedLongLong);CASE(float,Float);CASE(double,Double);
Let's not forget to undefine theCASE macro so we can reuse the name later:
#undefCASE
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:
[NSExceptionraise:NSInternalInconsistencyExceptionformat:@"Class %@ key %@ don't know how to interpret method return type from getter, signature is %@",[isadescription],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:
Ivarivar=class_getInstanceVariable(isa,[keyUTF8String]);
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 ofself:
if(ivar){ptrdiff_toffset=ivar_getOffset(ivar);char*ptr=(char*)self;ptr+=offset;
self is cast tochar * first, because the offset is in bytes, and operating on achar * ensures that the+= operation does what we need.
We also need to know the type of the variable:
constchar*type=ivar_getTypeEncoding(ivar);
If the type is an object or class, then it just extracts the value directly and returns it:
constchar*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 differentCASE macro. This one checks the type and then extracts the value fromptr if there's a match:
else{#defineCASE(ctype,selectorpart) \if(strcmp(type,@encode(ctype))==0) \return[NSNumbernumberWith##selectorpart:*(ctype*)ptr];
Once again, there's a long list of supported types:
CASE(char,Char);CASE(unsignedchar,UnsignedChar);CASE(short,Short);CASE(unsignedshort,UnsignedShort);CASE(int,Int);CASE(unsignedint,UnsignedInt);CASE(long,Long);CASE(unsignedlong,UnsignedLong);CASE(longlong,LongLong);CASE(unsignedlonglong,UnsignedLongLong);CASE(float,Float);CASE(double,Double);
Followed by macro cleanup:
#undefCASE
This code falls back to creating a genericNSValue with the contents ofptr 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[NSValuevalueWithBytes:ptrobjCType: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:
[NSExceptionraise:NSInternalInconsistencyExceptionformat:@"Class %@ is not key-value compliant for key %@",[isadescription],key];returnnil;}
That takes care ofvalueForKey:.
setValue:forKey:
ThesetValue: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)valueforKey:(NSString*)key{NSString*setterName=[NSStringstringWithFormat:@"set%@:",[keycapitalizedString]];
It then turns that into a selector and checks to see if the object responds:
SELsetterSEL=NSSelectorFromString(setterName);if([selfrespondsToSelector:setterSEL]){
If it does, it fetches the method's argument type andIMP much like the getter code above:
NSMethodSignature*sig=[selfmethodSignatureForSelector:setterSEL];chartype=[siggetArgumentTypeAtIndex:2][0];IMPimp=[selfmethodForSelector:setterSEL];
If the type is an object or class, it simply calls the setter, passingvalue, 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 aCASE macro. This one calls theIMP, passing[value typeValue] as the parameter, when a match is found:
else{#defineCASE(ctype,selectorpart) \if(type==@encode(ctype)[0]){ \((void(*)(id,SEL,ctype))imp)(self,setterSEL,[valueselectorpart##Value]); \return; \}
Here is the big list of cases:
CASE(char,char);CASE(unsignedchar,unsignedChar);CASE(short,short);CASE(unsignedshort,unsignedShort);CASE(int,int);CASE(unsignedint,unsignedInt);CASE(long,long);CASE(unsignedlong,unsignedLong);CASE(longlong,longLong);CASE(unsignedlonglong,unsignedLongLong);CASE(float,float);CASE(double,double);
Followed by macro cleanup:
#undefCASE
Last, if the type is unknown, it throws an exception:
[NSExceptionraise:NSInternalInconsistencyExceptionformat:@"Class %@ key %@ set from incompatible object %@",[isadescription],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:
Ivarivar=class_getInstanceVariable(isa,[keyUTF8String]);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 likevalueForKey: does:
if(ivar){ptrdiff_toffset=ivar_getOffset(ivar);char*ptr=(char*)self;ptr+=offset;constchar*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 minorretainrelease 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=[valueretain];[*(id*)ptrrelease];*(id*)ptr=value;return;}
Otherwise,value is boxed, and the primitive value needs to be extracted. Ifvalue is anNSValue with theexact same type as the instance variable, thegetValue: method can be used to simply copy the value over directly:
elseif(strcmp([valueobjCType],type)==0){[valuegetValue:ptr];return;}
If that doesn't work, it's time to fall back to the last long list of cases. This version of theCASE macro sets the value atptr, appropriately cast, to[value typeValue]:
else{#defineCASE(ctype,selectorpart) \if(strcmp(type,@encode(ctype))==0){ \*(ctype*)ptr=[valueselectorpart##Value]; \return; \}
The traditional exhaustive enumeration of primitive types follows:
CASE(char,char);CASE(unsignedchar,unsignedChar);CASE(short,short);CASE(unsignedshort,unsignedShort);CASE(int,int);CASE(unsignedint,unsignedInt);CASE(long,long);CASE(unsignedlong,unsignedLong);CASE(longlong,longLong);CASE(unsignedlonglong,unsignedLongLong);CASE(float,float);CASE(double,double);
Macro cleanup:
#undefCASE
Finally, if none of the cases were hit, throw an exception:
[NSExceptionraise:NSInternalInconsistencyExceptionformat:@"Class %@ key %@ set from incompatible object %@",[isadescription],key,value];}}
If neither setter method nor instance variable was found, throw an exception to complain:
[NSExceptionraise:NSInternalInconsistencyExceptionformat:@"Class %@ is not key-value compliant for key %@",[isadescription],key];}
Key Paths
To round out the implementation of KVC, I'll implementvalueForKeyPath: andsetValue:forKeyPath: as well.
The first thing thatvalueForKeyPath: does is look for a. in the key path. If it doesn't exist, then it's treated as a plain key and passed tovalueForKey:
-(id)valueForKeyPath:(NSString*)keyPath{NSRangerange=[keyPathrangeOfString:@"."];if(range.location==NSNotFound)return[selfvalueForKey: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=[keyPathsubstringToIndex:range.location];NSString*rest=[keyPathsubstringFromIndex:NSMaxRange(range)];
The key is passed tovalueForKey:
idnext=[selfvalueForKey:key];
ThenvalueForKeyPath: is sent recursively to thenext object:
return[nextvalueForKeyPath:rest];}
Its implementation will decomposerest further until every. is consumed. The result is a chain ofvalueForKey: calls, returning the result of the very last call.
setValue:forKeyPath: works similarly. If there's no. in the key path, callsetValue:forKey: and return:
-(void)setValue:(id)valueforKeyPath:(NSString*)keyPath{NSRangerange=[keyPathrangeOfString:@"."];if(range.location==NSNotFound){[selfsetValue:valueforKey:keyPath];return;}
Otherwise, extract the key and remainder:
NSString*key=[keyPathsubstringToIndex:range.location];NSString*rest=[keyPathsubstringFromIndex:NSMaxRange(range)];
Grab the next object usingvalueForKey:
idnext=[selfvalueForKey:key];
Then recursively sendsetValue:forKeyPath: tonext, passingrest as the key path:
[nextsetValue:valueforKeyPath:rest];}
The result is a chain ofvalueForKey: calls, culminating in a call tosetValue:forKey: on the last object in the chain.
Conclusion
You 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 likeNSInvocation for more comprehensive coverage, but that's the basic idea. A large part ofNSInvocation 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, pleasesend in your ideas for topics!
o.property nor[o property] works, but[o valueForKey:@"property"] does indeed work (this article helped me understand that it is because of the property->ivar->underscored ivar fallback).setValue:forKey: code has a tiny mistake:[@"helloYou" capitalizedString] gives@"Helloyou", not@"HelloYou", so you won’t find the desired setter method…is, thus the complete search path is: setter/getter ->_<Key> -> _is<Key> -> <Key> -> is<Key>.Add your thoughts, post a comment:
Spam and off-topic posts will be deleted without notice. Culprits may be publicly humiliated at my sole discretion.