Forge Code

Compile Time Checking of KVC Keys

KVC (Key Value Coding) is a pretty great technology. It’s used throughout the Cocoa APIs, and you might not even realise you’re using it. One of the more common tasks which makes use of KVC is sorting with NSSortDescriptor objects. Here’s a quick example snippet:

@interface Person : NSObject
@property () NSUInteger age;
@end

...

NSArray *people;  // Contains Person objects
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:YES]
NSArray *peopleSortedByAge = [people sortedArrayUsingDescriptors:[NSArray arrayWithObject:sortDescriptor]];

One of the great aspects to using a sort descriptor with KVC like this is that you don’t have to worry about the type that -age will return. It could be an int, an NSString or an NSNumber, and KVC and NSSortDescriptor will take care of it for you.

The Problem: String Literals

There’s one really big downside to KVC though, which is that you need to use string literals for the ‘Key’ part of ‘Key Value Coding’. This makes your code fragile in a number of ways.

  • You need to specify them in multiple places
  • Renaming the property or method means you have to update multiple strings
  • Typos won’t be discovered until run-time, when you’ll get an exception

One approach to try and reduce these problems is to create a constant string for every key you want to use with KVC. This solves the first issue, reduces the second one (you still have to remember to update the constant key string when re-factoring), but doesn’t solve the third.

A little while back, Uli Kusterer published a blog post describing a macro that, when used with the -Wundeclared-selector compiler flag, would throw a compiler warning for keys that don’t exist as a method.

#define PROPERTY(propName)    NSStringFromSelector(@selector(propName))


// usage:
NSSortDescriptor *ageDescriptor = [NSSortDescriptor sortDescriptorWithKey:PROPERTY(age) ascending:YES];

This solves all of the problems with string literals above, but has one other problem; you will only get a warning if the key does not match up with a selector on any class. This means that the following will compile without warning, but throw an exception at run-time:

@interface Polygon : NSObject

@property (strong) NSArray *lengths;

@end


@interface Line : NSObject

@property () CGFloat length;

@end

...

NSArray *lines;   // Contains Line objects
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:PROPERTY(lengths) ascending:YES];
NSArray *sortedLines = [lines sortedArrayUsingDescriptors:[NSArray arrayWithObject:sortDescriptor]];

The Key() Macro

In order to be confident that you’re not going to get an exception at run-time, you need to check that instances of a particular class respond to the selector, not just check that the selector has been declared. Recently I decided to dedicate a few hours to trying to come up with a macro that you can use like this:

sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:Key(Line, length)];

The first breakthrough in trying to create such a macro came when I realised that you could get a compile-time check of whether or not a class responds to a selector, while not actually causing anything to happen at run-time, by using a typecast nil:

[(Line *)nil length];   // Compiles Fine
[(Line *)nil lengths];  // Compiler Warning

You can wrap this up in a macro like this:

#define Key(class, key)      [(class *)nil key]

But the macro needs to not only check that the class responds to the key, it needs to return the key as an NSString. As far as I knew at the time, it was impossible to write a C macro that executed one expression, and returned the value of a second expression. After a few hours of research and experimentation, I discovered a neat trick: you can use C’s ternary operator to do exactly this, like so:

#define MyMacro(arg)     (expr1(arg) ? expr(arg) : expr2(arg))

This macro will run expr1(arg), and return the value of expr2(arg). This trick can be combined with the typecast nil trick, to give use a fully functional Key() macro!

The Actual Macro

#define Key(class, key)      ([(class *)nil key] ? @#key : @#key)

#define ProtocolKey(protocol, key)   ([(id <protocol>)nil key] ? @#key : @#key)

I’ve also shown a variant that allows you to get a KVC key for a method declared on a protocol, not on a class itself.

In theory, using these macros is slightly slower than just using a string literal (or constant string), but the overhead is so tiny (messaging nil, then evaluating a single ternary branch) that it should be negligible. With compiler optimisation turned on, the ternary branch should (I hope) be optimised out, and the messaging nil part may also be optimised out (although again, the performance hit should be negligible if it isn’t). Despite that, I have used an ifdef to shortcut the macro in Release builds:

#ifdef DEBUG
#define Key(class, key)              ([(class *)nil key] ? @#key : @#key)
#define ProtocolKey(protocol, key)   ([(id <protocol>)nil key] ? @#key : @#key)
#else
#define Key(class, key)              @#key
#define ProtocolKey(class, key)      @#key
#endif

Now you can safely use KVC with compile-time checking. If you aren’t using ARC, you will need to enable the -Wundeclared-selector compiler warning (it shows up in ‘Build Settings’ as ‘Undeclared Selector’ in recent versions of Xcode 4). Under ARC, you don’t need to turn this warning on for these macros to work - undeclared selectors will cause build errors no matter what (which is probably better for this particular situation). Hopefully you find these macros as useful as I do!

Comments