Re-binding self: the debugger's break(ing) point

Update 07-29-2019: The bug described below is fixed in Xcode 11 so this blog post has become irrelevant. I'm leaving it up for historical purposes.

For the Objective-C veterans in the audience, the strong-self-weak-self dance is a practice mastered early on and one that is used very frequently. There are a lot of different incantations, but the most basic one goes something like this:

1
2
3
4
__weak typeof(self) weakSelf = self;
dispatch_group_async(dispatch_get_main_queue(), ^{
    [weakSelf doSomething];
});

Then, if you needed a strong reference to self again inside the block, you'd change it to this:

1
2
3
4
5
__weak typeof(self) weakSelf = self;
dispatch_group_async(dispatch_get_main_queue(), ^{
    typeof(weakSelf) strongSelf = weakSelf;
    [strongSelf.someOtherObject doSomethingWith:strongSelf];
});

Fortunately, this was much easier on day 1 of Swift when using the [weak self] directive:

1
2
3
4
5
DispatchQueue.main.async { [weak self] in
    if let strongSelf = self {
        strongSelf.someOtherObject.doSomething(with: strongSelf)
    }
}

self is now weak inside the closure, making it an optional. Unwrapping it into strongSelf makes it a non-optional while still avoiding a retain cycle. It doesn't feel very Swifty, but it's not terrible.

More recently, it's become known that Swift supports re-binding self if you wrap it in backticks. That makes for an arguably much nicer syntax:

1
2
3
4
DispatchQueue.main.async { [weak self] in
    guard let `self` = self else { return }
    self.someOtherObject.doSomething(with: self)
}

This was long considered, and confirmed to be, a hack that worked due to a bug in the compiler, but since it worked and there weren't plans to remove it, people (including us at Lyft) started treating it as a feature.

However, there is one big caveat: the debugger is entirely hosed for anything you do in that closure. Ever seen an error like this in your Xcode console?

1
2
3
error: warning: <EXPR>:12:9: warning: initialization of variable '$__lldb_error_result' was never used; consider replacing with assignment to '_' or removing it
    var $__lldb_error_result = __lldb_tmp_error
        ~~~~^~~~~~~~~~~~~~~~~~~~

That's because self was re-bound. This is easy to reproduce: create a new Xcode project and add the following snippet to viewDidLoad():

1
2
3
4
5
6
DispatchQueue.main.async { [weak self] in
    guard let `self` = self else { return }

    let description = self.description
    print(description) // set a breakpoint here
}

When the breakpoint hits, execute (lldb) po description and you'll see the error from above. Note that you're not even using self - merely re-binding it makes the debugger entirely useless inside that scope.

People with way more knowledge of LLDB than I do can explain this in more detail (and have), but the gist is that the debugger doesn't like self's type changing. At the beginning of the closure scope, the debugging context assumes that self's type is Optional, but it is then re-bound to a non-optional, which the debugger doesn't know how to handle. It's actually pretty surprising the compiler supports changing a variable's type at all.

Because of this problem, at Lyft we have decided to eliminate this pattern entirely in our codebases, and instead re-bind self to a variable named this.

If you do continue to use this pattern, note that in a discussion on the Swift forums many people agreed that re-binding self should be supported by the language without the need for backticks. The pull request was merged shortly after and with the release of Swift 4.2 in the fall, you'll be able to use guard let self = self else { return } (at the cost of losing debugging capabilities!)