You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Currently Swift requires weak stored variables to be mutable.
14
-
This restriction is rather artificial, and causes friction with sendability checking.
15
+
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
16
17
## Motivation
17
18
18
-
Currently swift classes with weak stored properties cannot be `Sendable`,
19
-
because weak properties have to be mutable, and mutable properties are
20
-
not allowed in `Sendable` classes.
19
+
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:
21
20
22
-
Similarly, closures with `weak` captures cannot be `@Sendable`,
23
-
because such captures are implicitly made mutable.
21
+
```swift
22
+
finalclassC: Sendable {}
24
23
25
-
Usually developers are not aware of this implicit mutability and have no intention to modify the captured variable.
26
-
Implicit mutability of weak captures is inconsistent with `unowned` or default captures.
24
+
finalclassVarUser: Sendable {
25
+
weakvar ref1: C?// error: stored property 'ref1' of 'Sendable'-conforming class 'VarUser' is mutable
26
+
}
27
+
```
27
28
28
-
Wrapping weak reference into a single-field struct, allows stored properties and captures to be immutable.
29
+
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
30
31
```swift
31
-
finalclassC: Sendable {}
32
+
funcmakeClosure() ->@Sendable () ->Void {
33
+
let c =C()
34
+
return { [weak c] in
35
+
c?.foo() // error: reference to captured var 'c' in concurrently-executing code
36
+
37
+
c =nil// allowed, but surprising and very rare
38
+
}
39
+
}
40
+
```
32
41
42
+
In both cases, allowing the weak reference to be immutable would solve the problem, but this is not currently allowed:
43
+
44
+
```swift
45
+
finalclassLetUser: Sendable {
46
+
weaklet ref1: C?// error: 'weak' must be a mutable variable, because it may change at runtime
47
+
}
48
+
```
49
+
50
+
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.
51
+
52
+
In fact, wrapping weak references in a single-property `struct` is a viable workaround to the `var` restriction in both properties and captures:
53
+
54
+
```swift
33
55
structWeakRef {
34
56
weakvar ref: C?
35
57
}
36
58
37
-
finalclassUser: Sendable {
38
-
weaklet ref1: C?// error: 'weak' must be a mutable variable, because it may change at runtime
39
-
let ref2: WeakRef // ok
59
+
finalclassWeakStructUser: Sendable {
60
+
let ref: WeakRef // ok
40
61
}
41
62
42
63
funcmakeClosure() ->@Sendable () ->Void {
43
64
let c =C()
44
-
return { [weak c] in
45
-
c?.foo() // error: reference to captured var 'c' in concurrently-executing code
46
-
c =nil// nobody does this
47
-
}
48
65
return { [c =WeakRef(ref: c)] in
49
66
c.ref?.foo() // ok
50
67
}
51
68
}
52
69
```
53
70
54
-
Existence of this workaround shows that ban on `weak let` variables is artificial, and can be lifted.
71
+
The existence of this simple workaround is itself an argument that the prohibition of `weak let` is not enforcing some fundamentally important rule.
72
+
73
+
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.
55
74
56
-
Note that resetting weak references on object destruction is different from regular variable modification.
57
-
Resetting on destruction is implemented in a thread-safe manner, and can safely coexist with concurrent reads or writes.
58
-
But regular writing to a variable requires exclusive access to that memory location.
75
+
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.
59
76
60
77
## Proposed solution
61
78
62
-
Allow `weaklet`declarations for local variables and stored properties.
79
+
`weak` can now be freely combined with `let`in any position that `weak var` would be allowed.
63
80
64
-
Proposal maintains status quo regarding use of`weak` on function arguments and computed properties:
65
-
*there is no valid syntax to indicate that function argument is a weak reference;
66
-
*`weak` on computed properties is allowed, but has not effect.
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.
67
84
68
-
Weak captures are immutable under this proposal. If mutable capture is desired,
69
-
mutable variable need to be explicit declared and captured.
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`:
c =nil// error: cannot assign to value: 'c' is an immutable capture
78
94
}
95
+
}
79
96
97
+
funcmakeNonSendableClosure() -> () ->Void {
98
+
let c =C()
80
99
weakvar explicitlyMutable: C?= c
81
100
// Closure cannot be @Sendable anymore
82
101
return {
83
102
explicitlyMutable?.foo()
84
-
explicitlyMutable =nil//but assigned is ok
103
+
explicitlyMutable =nil// ok
85
104
}
86
105
}
87
106
```
88
107
89
108
## Source compatibility
90
109
91
-
Allowing `weak let` bindings is a source-compatible change
92
-
that makes previously invalid code valid.
110
+
Allowing `weak let` bindings is an additive change that makes previously invalid code valid. It is therefore perfectly source-compatible.
93
111
94
-
Treating weak captures as immutable is a source-breaking change.
95
-
Any code that attempts to write to the capture will stop compiling.
112
+
Treating weak captures as immutable is a source-breaking change. Any code that attempts to write to the capture will stop compiling.
96
113
The overall amount of such code is expected to be small.
97
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
+
98
117
## ABI compatibility
99
118
100
-
This is an ABI-compatible change.
119
+
There is no ABI impact of this change.
101
120
102
121
## Implications on adoption
103
122
104
-
This feature can be freely adopted and un-adopted in source
105
-
code with no deployment constraints and without affecting source or ABI
106
-
compatibility.
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