Skip to content

Commit dd51b7c

Browse files
authored
Merge pull request #2771 from nickolas-pohilets/mpokhylets/weak-let
Proposal to unban weak let bindings
2 parents c336eab + c173228 commit dd51b7c

File tree

1 file changed

+123
-0
lines changed

1 file changed

+123
-0
lines changed

proposals/0481-weak-let.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Feature name
2+
3+
* Proposal: [SE-0481](0481-weak-let.md)
4+
* Authors: [Mykola Pokhylets](https://github.com/nickolas-pohilets)
5+
* Review Manager: [John McCall](https://github.com/rjmccall)
6+
* Status: **Active review (April 30th...May 13th, 2025)**
7+
* Implementation: [swiftlang/swift#80440](https://github.com/swiftlang/swift/pull/80440)
8+
* Review: ([discussion](https://forums.swift.org/t/weak-captures-in-sendable-sending-closures/78498)) ([pitch](https://forums.swift.org/t/pitch-weak-let/79271))
9+
10+
[SE-0302]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0302-concurrent-value-and-concurrent-closures.md
11+
12+
## Introduction
13+
14+
Swift provides weak object references using the `weak` modifier on variables and stored properties. Weak references become `nil` when the object is destroyed, causing the value of the variable to seem to change. Swift has therefore always required `weak` references to be declared with the `var` keyword rather than `let`. However, that causes unnecessary friction with [sendability checking][SE-0302]: because weak references must be mutable, classes and closures with such references are unsafe to share between concurrent contexts. This proposal lifts that restriction and allows `weak` to be combined with `let`.
15+
16+
## Motivation
17+
18+
Currently, Swift classes with weak stored properties cannot be `Sendable`, because weak properties have to be mutable, and mutable properties are not allowed in `Sendable` classes:
19+
20+
```swift
21+
final class C: Sendable {}
22+
23+
final class VarUser: Sendable {
24+
weak var ref1: C? // error: stored property 'ref1' of 'Sendable'-conforming class 'VarUser' is mutable
25+
}
26+
```
27+
28+
Similarly, closures with explicit `weak` captures cannot be `@Sendable`, because such captures are implicitly *made* mutable, and `@Sendable` closures cannot capture mutable variables. This is surprising to most programmers, because every other kind of explicit capture is immutable. It is extremely rare for Swift code to directly mutate a `weak` capture.
29+
30+
```swift
31+
func makeClosure() -> @Sendable () -> Void {
32+
let c = C()
33+
return { [weak c] in
34+
c?.foo() // error: reference to captured var 'c' in concurrently-executing code
35+
36+
c = nil // allowed, but surprising and very rare
37+
}
38+
}
39+
```
40+
41+
In both cases, allowing the weak reference to be immutable would solve the problem, but this is not currently allowed:
42+
43+
```swift
44+
final class LetUser: Sendable {
45+
weak let ref1: C? // error: 'weak' must be a mutable variable, because it may change at runtime
46+
}
47+
```
48+
49+
The restriction that weak references have to be mutable is based on the idea that the reference is mutated when the referenced object is destroyed. Since it's mutated, it must be kept in mutable storage, and hence the storage must be declared with `var`. This way of thinking about weak references is problematic, however; it does not work very well to explain the behavior of weak references that are components of other values, such as `struct`s. For example, a return value is normally an immutable value, but a `struct` return value can contain a weak reference that may become `nil` at any point.
50+
51+
In fact, wrapping weak references in a single-property `struct` is a viable workaround to the `var` restriction in both properties and captures:
52+
53+
```swift
54+
struct WeakRef {
55+
weak var ref: C?
56+
}
57+
58+
final class WeakStructUser: Sendable {
59+
let ref: WeakRef // ok
60+
}
61+
62+
func makeClosure() -> @Sendable () -> Void {
63+
let c = C()
64+
return { [c = WeakRef(ref: c)] in
65+
c.ref?.foo() // ok
66+
}
67+
}
68+
```
69+
70+
The existence of this simple workaround is itself an argument that the prohibition of `weak let` is not enforcing some fundamentally important rule.
71+
72+
It is true that the value of a `weak` variable can be observed to change when the referenced object is destroyed. However, this does not have to be thought of as a mutation of the variable. A different way of thinking about it is that the variable continues to hold the same weak reference to the object, but that the program is simply not allowed to observe the object through that weak reference after the object is destroyed. This better explains the behavior of weak references in `struct`s: it's not that the destruction of the object changes the `struct` value, it's that the weak reference that's part of the `struct` value will now return `nil` if you try to observe it.
73+
74+
Note that all of this relies on the fact that the thread-safety of observing a weak reference is fundamentally different from the thread-safety of assigning `nil` into a `weak var`. Swift's weak references are thread-safe against concurrent destruction: well-ordered reads and writes to a `weak var` or `weak let` will always behave correctly even if the referenced object is concurrently destroyed. But they are not *atomic* in the sense that writing to a `weak var` will behave correctly if another context is concurrently reading or writing to that same `var`. In this sense, a `weak var` is like any other `var`: mutations need to be well-ordered with all other accesses.
75+
76+
## Proposed solution
77+
78+
`weak` can now be freely combined with `let` in any position that `weak var` would be allowed.
79+
Similar to `weak var`, `weak let` declarations also must be of `Optional` type.
80+
81+
This proposal maintains the status quo regarding `weak` on function arguments and computed properties:
82+
* There is no valid syntax to indicate that function argument is a weak reference.
83+
* `weak` on computed properties is allowed, but has no effect.
84+
85+
An explicit `weak` capture is now immutable under this proposal, like any other explicit capture. If the programmer really needs a mutable capture, they must capture a separate `weak var`:
86+
87+
```swift
88+
func makeClosure() -> @Sendable () -> Void {
89+
let c = C()
90+
// Closure is @Sendable
91+
return { [weak c] in
92+
c?.foo()
93+
c = nil // error: cannot assign to value: 'c' is an immutable capture
94+
}
95+
}
96+
97+
func makeNonSendableClosure() -> () -> Void {
98+
let c = C()
99+
weak var explicitlyMutable: C? = c
100+
// Closure cannot be @Sendable anymore
101+
return {
102+
explicitlyMutable?.foo()
103+
explicitlyMutable = nil // ok
104+
}
105+
}
106+
```
107+
108+
## Source compatibility
109+
110+
Allowing `weak let` bindings is an additive change that makes previously invalid code valid. It is therefore perfectly source-compatible.
111+
112+
Treating weak captures as immutable is a source-breaking change. Any code that attempts to write to the capture will stop compiling.
113+
The overall amount of such code is expected to be small.
114+
115+
Since the captures of a closure are opaque and cannot be observed outside of the closure, changing the mutability of weak captures has no impact on clients of the closure.
116+
117+
## ABI compatibility
118+
119+
There is no ABI impact of this change.
120+
121+
## Implications on adoption
122+
123+
This feature can be freely adopted and un-adopted in source code with no deployment constraints and without affecting source or ABI compatibility.

0 commit comments

Comments
 (0)