Key–value coding — valueForKeyPath:
and setValue:forKeyPath:
— is very useful for converting data from one format to another, such as copying from a property list-like structure (e.g. deserialized JSON) into model objects proper.
This avoids a lot of boilerplate typically found in, for example, Java-based systems1.
On the Mac, with Cocoa Bindings you can throw together a simple UI in a flash, setting up key paths in Interface Builder. On iOS you have to provide some of the glue yourself, but you typically let key–value observing do the hardest work.
The problem
These “key–value” techniques have one main thing in common: They use strings to reference code. This makes it fragile. For example:
@implementation ContrivedExample
- (NSString *)title
{ return @"foo"; }
- (NSUInteger)contrivedLength
{ return [[self title] length]; }
- (NSUInteger)contrivedLengthKVC
{ return [[self valueForKey:@"title"] length]; }
@end
If we rename the method title
, the compiler will immediately show that contrivedLength
won’t work without changes.
But contrivedLengthKVC
will have no such warning, because @"title"
remains a perfectly valid string.
We would only see an error at run-time, if and when that code was triggered.
Of course in real code, string references like this appear much further apart than in the above snippet, making stale references and even typos a real problem.
The common solution
In short: The compiler does not validate key paths, because they are strings.
One approach toward validation is avoid strings2. Of course, a string must emerge at some point, but pre-processor macros allow using the same expression (title
) as both code and data, at compile-time.
Many have taken this route, resulting in a large variety3 with different trade-offs. A couple that are representative:
- With libextobjc you have to specify the target of the key path twice —
[self valueForKey:@keypath(self.title)]
above — though this is often redundant, and obscures that the resulting string will be simply “title”. - DMSafeKVC is more concise at use —
[self valueForKey:K(title)]
— but require annotating declarations, and only checks that some class has an annotated “title” accessor.
Compiler error messages are typically obscured due to macro expansion. And if you’re writing library code4, often the best approach is to use the lowest common denominator — static strings.
A few months back, my colleagues at Fitbit discussed the ups and downs of various macro implementations and didn’t arrive at consensus. Additionally, retrofitting our codebase would be a mostly manual task and likely generate many conflicts (our repository has around 10 active committers).
I began to consider a different approach.
The code less traveled
The compiler does not validate key paths, because they are strings. In our phrasing of the problem, a second solution becomes apparent: make the compiler validate the strings!
In the past, touching the compiler has been considered off-the-table for various reasons. With clang, this is now more practical5, but the previous stigma still lingers.
This code is currently proof-of-concept quality. (I’m only a C++ novice, and barely familiar with the clang codebase.) It currently:
- Checks some hard-coded method calls that are passed string literal arguments (including macros that expand to string literals).
- Validates that there are corresponding getter methods, recursing down the key path.
- Checks for a method named
key
orisKey
, or a collection property that might be backed with KVC collection accessor methods. - Does no checking if the type is
id
,NSDictionary
, a class annotated withobjc_kvc_container
, or a few more. - Knows that scalar numbers will be returned as
NSNumber
. - Uses the diagnostic output of the secondary clang, but the binary output of Xcode’s bundled clang.
The compiler plug-in doesn’t do everything that some of the macro approaches do, but has the giant advantage of requiring zero code changes and no dependencies. It also doesn’t exclude the use of macros, either. Only one small project setting change is required:
If you’d like to try this with your own project, look at the Clang-KeyPathValidator README file. Then check out the GitHub Issues page and see if you can help out with something. Or just help get the word out!
-
This opinion is several years old, before Java reflection was commonly available or efficient. ↩
-
The term “stringly-typed” is just great. ↩
-
Uli Kusterer’s approach using
@selector
;
Nicolas Bouilleaud’s approach using@selector
;
Kyle Van Essen’s approach using declared targets;
Andrew Pouliot’s approach using declared targets;
Martin Kiss’s approach using declared targets;
and many more. ↩ -
“Library code” refers to code shared between projects which might use different safe KVC techniques, or none at all. ↩
-
It’s unfortunately still not completely painless, because the particular versions shipped by Apple with Xcode are not made publicly available, and have plug-in support disabled. ↩