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.

21 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.

6 Likes

I would attribute this to the fact that [weak self] is both easier to teach (in contrast to properly instructing how to use unowned self) and not as dramatic when it doesn't work (i.e., it doesn't crash apps immediately). Unfortunately though, these are not good indicators as to whether weak captures are actually the appropriate solution.

To counter your intuition: in one of my UI-heavy apps, there are only two instances of [weak self] captures, everything else is either a strong or an unowned reference, as I almost never encounter a case where [weak self] would be the right thing to do — so I would definitely prefer it to not be the implicit default.

1 Like
1 Like

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.

26 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.

Well, to me, from the philosophical standpoint at least, it would be strange to see Swift defaulting into a non-proper solution just because it fails silently if misused. And I would object the point that such default doesn't cause problems (especially combined with an implicit guard let, which would drop the execution of a whole closure completely); it just doesn't cause one common class of problems. I should not repeat what others previously said in former discussions and refer to their points:

However, the inclusion of an implicit guard let also raises another question: how do you opt out? There's no syntax for "un-guard-letting"; adding such would be an overcomplicated attempt to solve a problem we never had in the first place, and being unable to opt out is bad because sometimes you do want to perform some job before checking the nullity of self or do something else if self is nil — not stop executing completely.

The article fails to recognise the existence of unowned self, which can be equally or an even better solution to breaking a retain cycle, with the added benefit of checking an invariant, much as array bounds checking works. The phrasing that "using [weak self] can also be a good idea <...> [as otherwise it] will cause it to remain in memory for that same amount of time" sounds like a one-sided treatment to me, as there are numerous cases when you want to ensure the object stays in the memory as long as the closure.

To sum up my argument, a [weak self] capture is an "I don't care about lifetimes" statement; making it a default would help in cases when I indeed don't care, but more often than not, I do, and so I want the closure. From the language perspective, it seems logical that the language too should make you aware of them, as with many other things Swift chooses to make explicit.

8 Likes

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.

4 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?

Speaking strictly, it's quite impossible to tell what use case is the more typical one, because even if we ran a sort of a survey or analysed a number of codebases, many have been incorrectly adopting [weak self] in places where it shouldn't be used, as some (anecdotal) experience shows.

The common thread in the most objections to what you are proposing is that it would turn one "macro-problem" into a number of "micro-problems" that each on their own may indeed be rarer occurrences, but would result in such a huge inconsistency and lower debug-ability of the language that it doesn't seem worth it.

2 Likes

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.

1 Like

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.

1 Like

At this point it boils down to our personal definitions of harm. I consider it to be actively harmful to use weaker assertions than appropriate for the use case, since then otherwise I need to prove at least to myself that other possibilities are covered, instead of crashing at the point I'm certain I never intended to reach. Maybe that's due to me being a (mediocre, but still) mathematician, but I expect to be able to prove that my code works just by looking at it and only then running tests, so while normally people might get a heart attack from the sheer amount of implicit unwraps, preconditions and unowned references in my codebases, it helps me to make sure the code works as intended and makes it test itself as it runs, and it would really harm myself if I lost the ability to write it this way.

6 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?

2 Likes
Terms of Service

Privacy Policy

Cookie Policy