When updating the Angle example from SE-400 by changing the struct into an actor the warning "Actor-isolated property 'radians' can not be mutated from a non-isolated context; this is an error in Swift 6" is emitted for the second initializer.
actor Angle {
var degrees: Double
var radians: Double {
@storageRestrictions(initializes: degrees)
init(initialValue) {
degrees = initialValue * 180 / .pi
}
get { degrees * .pi / 180 }
set { degrees = newValue * 180 / .pi }
}
init(degrees: Double) {
self.degrees = degrees // initializes 'self.degrees' directly
}
init(radiansParam: Double) {
self.radians = radiansParam // calls init accessor for 'self.radians', passing 'radiansParam' as the argument
}
}
Is this a (already known) bug or limitation or expected behaviour?
I find this surprising as I would imagine that the init block of the computed property is executed within the initializers context.
That's a part of change within SE-0411. By default, synchronous init is non-isolated in actor/actor-isolated types, which causes this warning, which is correct – you are modifying isolated radians from non-isolated context. One way to fix this is to make init asynchronous (more details on that in SE-0327, specifically that case is described in "Initializers with isolated self"):
But do I really modify radians? radians is not a stored property and completely computed. The only thing that I am doing is initializing degrees via one indirection.
From my understanding the following code snippet is equivalent to the one in the original post:
actor Angle {
var degrees: Double
var radians: Double {
get { degrees * .pi / 180 }
set { degrees = newValue * 180 / .pi }
}
init(degrees: Double) {
self.degrees = degrees
}
init(radiansParam: Double) {
self.degrees = radiansParam * 180 / .pi
}
}
Yes, you are modifying - not exactly radians as value, but the type itself and have to assume any possible side-effect it might have on the type. As artificial example, imagine you want to save all the radians values that were passed initially, so you modify type as follows:
actor Angle {
var degrees: Double
var radians: Double {
@storageRestrictions(initializes: degrees, accesses: initialRadians)
init(initialValue) {
// here comes mutation from non-isolated context of isolated property
initialRadians.append(initialValue)
degrees = initialValue * 180 / .pi
}
get { degrees * .pi / 180 }
set { degrees = newValue * 180 / .pi }
}
private var initialRadians: [Double] = []
nonisolated init(degrees: Double) {
self.degrees = degrees
}
nonisolated init(radiansParam: Double) {
self.radians = radiansParam
}
}
Now your code mutates internal property of initialRadians from non-isolated context of init call. Swift cannot reason whether your computed property is safe or has no side-effects apart initialization since it is computed. Your code snipped with explicit initialization of degrees is not equivalent to the version with accessor init in that context, since there you have unambiguous initialization of degress property and no interaction with radians.
UPD. Maybe it could reason - in theory at least - since @storageRestrictions explicit about what properties it needs, but that complicates overall check, since now init has to check if accessor init just initializes a property and nothing else.
Swift applies definite initialization analysis to make sure that you only access values during initialization that are already initialized. That is also the reason why you need to be very specific about what you are going to initialize and access via @storageRestrictions. From my understanding there is no need to worry that the init block accesses something beside the values that @storageRestrictions defines, because self is not definitely initialized anyway when it is called. And when it is set is used and with set the warning makes sense.