Assertions are a great tool. As an Objective-C programmer, I use NSAssert
and NSCAssert
liberally.
For various reasons, you sometimes can’t use NSAssert
in a block easily. I’m going to explain why and describe a new macro, BlockAssert
, which solves this.
Background
An assertion is a predicate (a true–false statement) placed in a program to indicate that the developer thinks that the predicate is always true at that place. (Source: Wikipedia.)
NSAssert
and NSCAssert
are Foundation’s assertion macros. They’re kind of like C’s assert macro, but provide an error message in the same form as NSLog
. The difference between them is that NSAssert
references self
, which is only defined for Objective-C methods. NSCAssert
, then, is NSAssert
for C functions.
Blocks are often defined in Objective-C methods, but they’re not themselves Objective-C methods. Although they’re actually implemented as objects of their own, the syntax is more like a C functions. They don’t define self, though anything you want from the method scope are captured strongly into the block expression. This means that if you use NSAssert
you’ll be capturing self
. But that strong reference can cause a circular reference.
A circular reference is formed the block owns the object and the object owns something that owns the block. None of the memory will ever be cleaned up. Even you do clean up the memory, you’ll get a compiler warning that you need to ignore.
Rather than sort out which of these compiler warnings is valid, a lot of people use this pattern:
__weak typeof(self) weakSelf = self;
foo.completionBlock = ^{
__strong typeof(self) strongSelf = weakSelf;
[strongSelf doSomething];
};
This breaks the circular reference, and because we’ve used __weak
we’re assured that weakSelf
is not a dangling pointer. If self
no longer exists, weakSelf
is nil
. Reassigning back to a __strong
variable presents the variable from being deallocated
In this case, it’s not actually necessary to assign weakSelf
to a __strong
variable. The recipient of a message won’t be deallocated until the message returns. However, if you are doing multiple things with weakSelf
— such as doing assertions against it and sending it a message — it could become deallocated between those messages.
The catch? NSAssert
still uses the self
variable by name. This led to an interesting question on StackOverflow about defining a block-scope self, a technique that’s valid but requires you to remember to do it every time and causes a warning if GCC_WARN_SHADOW is on.
A new assertion macro
In the past, I’ve mostly done avoided this by not using assertions in blocks. You don’t need them very often, as you can usually do your assertions in the method’s scope. But inspired by that question, I’ve found a better way: I define my own assertion macro.
I have several such macros I define, in a single file that’s imported in as part of my precompiled header.
The macro looks like this:
#define BlockAssert(condition, desc, ...)
do {
__PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS
if (!(condition)) {
[[NSAssertionHandler currentHandler] handleFailureInMethod:_cmd
object:strongSelf file:[NSString stringWithUTF8String:__FILE__]
lineNumber:__LINE__ description:(desc), ##__VA_ARGS__];
}
__PRAGMA_POP_NO_EXTRA_ARG_WARNINGS
} while(0)
This is basically a copy-paste of NSAssert
, but uses strongSelf
instead of self
. (Note, however, that we have not pretended to be part of Foundation by using a NS prefix.)
To use BlockAssert
, you need to use the weakSelf = self; strongSelf = weakSelf;
pattern, but you probably already are. And if not, the compiler will throw an error: Use of undeclared identifier: ‘strongSelf’. You can either adopt strongSelf
or switch over to NSAssert
.
For example:
- (IBAction)clickButton: (UIButton *)sender {
sender.disabled = YES;
[self performActionWithCompletion: ^{
BlockAssert(state, @"Invalid state");
sender.disabled = NO;
}];
}
Conclusion
I think this is a pretty good approach: If you use NSAssert
where you meant to use BlockAssert
and it matters, you’ll get a warning about a potential circular reference. If you use BlockAssert
where you meant to use NSAssert
, you’ll probably get a compiler error.