In recent years, the clang compiler has added a number of source annotations to Objective-C in order to clarify API intent. For example, NS_DESIGNATED_INITIALIZER
formalizes the traditional Objective-C designated initializer pattern. To facilitate interoperability with Swift, Objective-C adopted nullibility annotations: nullable, _Nullable, nonnull, _Nonnull
. As with all changes, however, there are tradeoffs. The use of these annotations comes with a price. Consider the following (dummy) code illustrating the designated initializer pattern (and nothing else):
@import dnssd;
@import Foundation;
@interface MyService : NSObject
-(DNSServiceRef)service;
-(instancetype)initWithService:(DNSServiceRef)service; // designated initializer; service must be non-NULL
@end
@implementation MyService
{
DNSServiceRef _service;
}
-(DNSServiceRef)service
{
return _service;
}
-(instancetype)initWithService:(DNSServiceRef)service
{
CFRetain(service);
self = [super init];
_service = service;
return self;
}
-(instancetype)init
{
[NSException raise:NSInternalInconsistencyException format:@"Use the designated initializer, dummy!"];
return nil;
}
-(void)dealloc
{
CFRelease(_service);
}
@end
Without annotations, the designated initializer is indicated by a comment, as is the requirement that the DNSServiceRef
be non-NULL. The latter requirement is enforced at runtime. If you call -init
, it raises an internal inconsistency exception, and if you call -initWithService:
with a NULL argument, it crashes. You'll see *** CFRetain() called with NULL ***
in the crash log. You could add NSParameterAssert
for clarity, but the CFRetain
is sufficient to enforce the API.
Now let's add an annotation:
-(instancetype)initWithService:(DNSServiceRef)service NS_DESIGNATED_INITIALIZER; // service must be non-NULL
Suddenly we get a build warning:
convenience initializer missing a 'self' call to another initializer [-Wobjc-designated-initializers]
-(instancetype)init
The rule of designated initializers is that all initializers must eventually call the designated initializer. However, -init
isn't meant to be called at all, it raises an exception when called. Nonetheless, we can easily silence the warning:
-(instancetype)init
{
return [self initWithService:NULL];
}
The NULL argument causes a crash when called, so we've again enforced the API contract at runtime. Now let's add nullability annotations:
-(nonnull DNSServiceRef)service;
-(nonnull instancetype)initWithService:(nonnull DNSServiceRef)service NS_DESIGNATED_INITIALIZER;
This causes another build warning in that same pesky -init
method:
null passed to a callee that requires a non-null argument [-Wnonnull]
return [self initWithService:NULL];
Ugh. This leaves us with a dilemma. On the one hand, the NS_DESIGNATED_INITIALIZER
annotation forces -init
to call -initWithService:
, and on the other hand, the nonnull
disallows calling -initWithService:
with NULL.
There are ways to work around this problem, but I warn you now that they're going to make your eyes bleed, so please turn away if you're squeamish, and make your children leave the room. For those still with us, behold!
-(instancetype)init
{
return [self initWithService:(DNSServiceRef _Nonnull)NULL];
}
Yup. We're casting NULL to non-NULL. Seriously. No more needs to be said. Whereof one cannot speak, thereof one must be silent.
Ok, I lied. More needs to be said. After some … philosophical investigations … I found a better workaround:
-(instancetype)init NS_UNAVAILABLE;
Still, it's worth noting a logical consequence of our ability to cast NULL to _Nonnull
. The nullability annotations are compiler checks, not runtime checks. Their presence in an API does not prevent an unexpected NULL argument. You would still need to check at runtime to guarantee it's not happening.
The problems are solved, not by giving new information, but by arranging what we have known since long.