Why is implicit `[weak self]` not the norm?

After browsing the web for this question and learning about memory retain cycles in Swift (and how to deal with them), it is easy to get the impression that adding the capture list [weak self] is often the solution and rarely the cause of problems. I am myself admittedly guilty in adding [weak self] and guard let self = self else { return } to many of my closures if I'm uncertain that they could lead to retain cycles. Actually, the number of times where weak self is the solution (or in worst case not the cause of a problem) vastly outnumber the times where keeping a strong reference to self would be preferred. This got me to questioning: Why is [weak self] and guard let self = self else { return } not implicit in all closures (where self can be referenced)? If there are situations where you want to keep a strong reference, you could use [strong self], but isn't this use case vastly outnumbered by the number of time where [weak self] is preferred?
I hope someone could explain to me, why weak self in closures is not the norm (implicit) instead of strong references to self (from the perspective of the average programmer)?

6 Likes

I frequently encounter bugs caused by unnecessary reflexive [weak self] (and, anecdatally, I would say that incorrect uses of weak outweigh correct ones by about 10:1).

It's frequently the case that an effect in a closure must happen – for example, committing data to a persistent store, or signaling a semaphore – but this doesn't happen because of an inappropriately weak reference that is gone by the time the closure is called.

Making weak the implicit default would move responsibility for these errors from the user to the language, worsening the problem.

22 Likes

Is the mindset of "this must happen, therefore strong self", not easier to consider as a design choice compared to "is there a retain cycle here? Should I use weak self?"?

I.e. when you mention "incorrect uses of weak outweigh correct ones" (and I assume this includes guard self), do you mean incorrect as in 'unnecessary but harmless' or as in causing unintended behavior (bugs)?

I'm admittedly relatively new to Swift, using it mostly for app development, so my curiosity is based on the number of time I need to add repeated boilerplate code and expect other new developers to do the same. I understand that this might be a naive position, and that there are other focus areas where the inverse (explicit [weak self], as it is now) could be preferred. But without familiarity with these areas (and as a relatively new developer), boilerplate code does make you question the reasoning behind the design decisions of the language, or if this is something that will be improved in the future?

Browsing this topic online, you get the impression that dealing with retain cycles is by far a bigger issue for the majority of developers (try searching for people having issues with retain cycle and watch the number of recommendations to use [weak self], and in contrast with the number of people having issues or bugs caused by unnecessary use of [weak self]).

1 Like

The common case when [weak self] makes most sense is when the closure is just updating the UI. If the UI is no longer visible, then it's generally fine to just bail out. For some applications and some users, this "just updating the UI" case is the primary use case for completion handlers.

However, Swift is not exclusively used in "updating the UI" contexts, and the decisions around capturing self cannot assume it will be used in that way.

9 Likes

https://forums.swift.org/t/shouldnt-be-weak-self-the-default/29805

2 Likes

It's worth noting that the overall arc of both Swift (with the introduction of async and friends) and Swift frameworks (such as SwiftUI defining views as value types) is to define away the places you'd use weak, either correctly or incorrectly.

29 Likes

Yes. thats really great! Looking forward to this :slight_smile:

Yes, but the question is: In how many of those situations where you say [weak self] shouldn't be necessary or the right thing to do, would using [weak self] (and remember guard let self = self else { return } in contrast to unowned) result in a bug or unintended behavior? The point here is; if it does not cause problems when used unnecessarily, but solves a lot of common retain-cycle problems when it is forgotten (arguing that retain-cycles is a common issue when coding in Swift), shouldn't implicit [weak self] and guard let self = self else { return } be the norm, and intended [self] be explicit to reduce boilerplate code to the (assumed) majority of use cases where Swift is used?

I think these recommendations by John Sundell is quite interesting:

  • Using [weak self] is only required within situations in which capturing self strongly would end up causing a retain cycle, for example when self is being captured within a closure that’s also ultimately retained by that same object.
  • Using [weak self] can also be a good idea when working with closures that will be stored for a longer period of time, as capturing an object strongly within such a closure will cause it to remain in memory for that same amount of time.
  • In all other situations, using [weak self] is optional, but there’s typically no harm in adding it — unless we want to capture self strongly for some reason.

If there is "typically no harm in adding it", then there is typically no reason not to make [weak self] implicit, in situations where creating a strong reference cycle is something you want to avoid.

I'm not fond of this line of defensive thinking. Every time I tried to reason with the code that put up a boilerplate "just in case", I end up wasting more time than needed trying to figure all the code paths. Most of which are dead paths.

6 Likes

Yes, this is interesting. Using unowned self instead of guard let self = self else { return }, simply means that you prefer to crash out instead of attempting to finish execution of the closure before deallocation self (all for the sake of not having to type self?). To make implicit guard let self viable for opting out, there would need to be some way of specifying if a running closure — that was started when self was still allocated in memory — should be able to return mid execution if self "would be" deallocated while the closure is still being executed.

But here you would then use (explicit) [self] for these cases, if implicit [weak self] was implemented?

To sum up: The discussion is about how typical the average developer need to use [self] specifically, compared to how often [weak self] would be either preferred or harmless. This, to avoid boilerplate for the majority or "common use case". Or is this view on the topic naive?

I.e. if explicit [self] and implicit [weak self] was what we had today and was the norm, what would the good arguments be to change it?

FWIW, I don't think changes that will alter the meaning of an existing valid code (especially in common scenarios) are exactly on the table. (You'd at least need to demonstrate active harm in current behaviour.)

1 Like

I usually strongly advice against unowned captures, unless performance is really critical (which is almost never the case). It is like a mine: When you know it's there, there's little danger — but when you forget it, the effect can be devastating.
Favourite example: Making a copy of a closure with an unowned captures raises no warnings at all, yet it is very likely to cause a crash.

You can argue from a philosophical standpoint that weak captures are "bad", but reality is that many people default to the weak-strong dance, either because they have seen it so often in other developers code, or because they have been bitten by the alternatives themselves.

And why should they stop doing that? As others have pointed out, there's usually no real harm in defaulting to weak self.

However, I don't think changing the defaults is an option nowadays… but maybe it would be possible to extend what we have (like not only having self, but also an implicit variable weakSelf).

I think it is probably also easier to learn closures without weak as the default. Having a strong reference by default matches the rest of the language in typical everyday use, so it's nothing new.

One can learn closures today without having to know anything about retain cycles, which are more of an edge case. It would not be clear to a beginner why capturing (self or other reference types) gives you optional values.

At least this is just my personal experience learning from Paul Hudson.

2 Likes

As I admittedly use [weak self] and guard let self = self else { return } quite often with MVVM, especially with Combine, it would be great if there was a global function that could take a closure, and return the same closure with [weak self] and guard let self = self else { return } added...somehow. Then I could replace my {} closures with weakly {} or something similar.

3 Likes

How would this work? Should all captures be weak, or should self be special cased?

More often than not, a strong capture is really what you want (completion handlers, animation blocks, dispatches to other queues, alert button handlers, and other single-invocation closures).

And when you have multi-invocation closures such as signal handlers, event handlers, their lifetime is often tied to self , and you can use unowned capture.

2 Likes

One solution is to introduce a special syntax for blocks who capture all variables as weak. For example:

publisher.sink {? self.update(button) }

This can be equivalent to

publisher.sink { [weak self, weak button] in 
    guard let self = self else { return }
    guard let button = button else { return }
    self.update(button)
}

WDYT?

3 Likes

Something like this would be fantastic! :grinning::+1:

1 Like

Then I'll prepare a formal pitch with this solution to gather more feedback

1 Like
2 Likes

I'd personally prefer is blocks were to be upgraded to first class types so we could add operations to them and for example add an operation to blocks that makes them wrap themselves into another block that only runs the caller if the first parameter is live.

As a stopgap measure I have the following global function around but of course it doesn't scale that great nor does it look pretty.

func run<T, U, V>(ifLive object: T, block: (T, U) -> V) -> (U) -> V where T: AnyObject {
    return { [weak object] in
        guard let object = object else {
            return
        }

        return block(object, $0)
    }
}