In my “Core Data Potpourri” talk, I recommend declaring constant NSStrings of NSManagedObject attributes (that’s the correct Core Data term, but I’m just going to call them “properties” from here on out) for use in NSPredicates and KVC. Via Twitter, Riley Testut suggested an alternative:
@pgor have you considered using NSStringFromSelector(@selector(property)) when having to hardcode a property string? Seems cleanest to me
— Riley Testut (@rileytestut) March 22, 2014
This does look like it would be cleaner. There’s not a “magic string” involved, and with the “Undeclared Selector” warning (GCC_WARN_UNDECLARED_SELECTOR) you would be warned if a property changed and your @selector wasn’t updated. Upon closer examination, though, there are a few problems.
Problem the First: Declaration
The proposed construct is not a compile-time constant, so I can’t just change my
NSString* const EventKeyTimeStamp = @"timeStamp";
line to
NSString* const EventKeyTimeStamp = NSStringFromSelector(@selector(timeStamp));
There might be a way to pull out some compiler attribute-fu to initialize this properly, but I doubt it would be able to keep the const
qualifier that makes me feel safe.
The next step would be to eschew the constant variable declaration and just use a #define
in the header:
#define EventKeyTimeStamp NSStringFromSelector(@selector(timeStamp))
I’m not a fan of having actual code in #defines. They always seem to end up causing more trouble and obfuscation than they’re worth, particularly when I write them. I’d much rather just write a boring function or method that I can breakpoint and walk through as a normal part of my applicaton.
But what the heck, let’s go with the precompiler for this one. It works. It substitutes the proper string for KVC and predicates. If you rename the timeStamp
property to timestamp
it will complain if you don’t change the @selector. All looks well.
Problem the Second: Indiscriminate Selectors
The confidence we gain from this method is based on the undefined selector warning firing if we change the target property name. Simple testing bears that out but unfortunately that’s not the case.
The undefined selector warning will be satisfied if it sees a matching selector anywhere in the current compilation unit. You may think that because our #define is in the NSManagedObject’s header, that compilation unit is just the MO and the (hopefully limited) #imports from that header. Unfortunately, #defines are first substituted into the code in-place then evaluated. So when evaluated, its compilation unit includes every header the client code has imported—if any object in that unit has a matching selector, the warning will be satisfied.
If we rename -timeStamp to -timestamp and any object in the current compilation unit has a -timeStamp method, we will not be warned that we did not update this constant and it will fail in KVC and predicates at runtime.
Any object.
If you can guarantee that you can create some code in your project that will never experience that kind of cross-contamination, it can be the alert system for validating your property constants. If not, the confidence using NSStringFromSelector in this case is sadly most likely false confidence.
At this point, I see no benefit to NSStringFromSelector in this manner. Admittedly, my constant string mechanism is no more reliable in alerting me of these problems, but my aversion to code in #define statements will keep me using constant strings.
Unit Tests!
If you really want safety for this (and you should), I still believe that unit tests are the most reliable way to do it.
- (void) setUp
{
[super setUp];
self.managedObjectContext = [MyTestContext createInMemory];
}
- (void) testAttributes
{
Event* event = [Event insertNewObjectInManagedObjectContext:self.managedObjectContext];
NSDate* testDate = [NSDate date];
event.timeStamp = testDate;
XCTAssertEqualObjects(event.timeStamp, testDate, @"timeStamp property failed to store properly");
XCTAssertNoThrow([event valueForKey:EventKeyTimeStamp], @"KVO property '%@' doesn't exist", EventKeyTimeStamp);
XCTAssertEqualObjects([event valueForKey:EventKeyTimeStamp], event.timeStamp, @"KVO value differs from property");
}