Reputation: 3385
I want to create a class that conforms to sendable that holds an array of Ints. However I get the following error:
Stored property '_counters' of 'Sendable'-conforming class 'Manager' is mutable
Could you point me to how to resolve this. thanks
@Observable final class Manager:Sendable{
var hasContent:Bool{
return counters.count > 0
}
private var counters:[Int] = []
func lastNumber()->Int?{
return counters.count == 0 ? nil : counters.last
}
func addCounter(){
let number = Int.random(in: 1...50 )
counters.append(number)
}
}
Upvotes: 1
Views: 281
Reputation: 273540
As others have said in the comments, this is a very unusual thing to do. Rather than sending the whole Manager
to another concurrency context, consider sending just the data you need, as a value type. For example, in this case you should just send over the counters
as a simple [Int]
.
Suppose you want to send a Manager
from a main actor isolated context to a non-isolated context by passing it to a non-isolated function doSomething
.
func doSomething(_ x: Manager) async { ... }
@MainActor
func caller(_ manager: Manager) async {
await doSomething(manager)
}
You should change doSomething
to take a [Int]
instead,
func doSomething(_ x: [Int]) async { ... }
@MainActor
func caller(_ manager: Manager) async {
await doSomething(manager.counters)
}
If the other concurrency context needs to modify the data, you can design it so that it sends back the changes too. e.g. doSomething
could return a [Int]
indicating what the new counters
should be.
func doSomething(_ x: [Int]) async -> [Int] { ... }
@MainActor
func caller(_ manager: Manager) async {
manager.counters = await doSomething(manager.counters)
}
doSomething
could also return an AsyncStream
/AsyncChannel
that caller
can consume. As the work progresses, doSomething
would send elements down the stream. The UI can show the current state of a long-running operation this way.
If you really want to send a reference type to another concurrency context, send an actor
. Then, after whatever needs to be done is done, update your @Observable
class with the state of the actor.
func doSomething(_ x: ActorManager) async { }
@MainActor
func caller(_ manager: Manager) async {
let actorManager = ActorManager(counters: manager.counters)
await doSomething(actorManager)
manager.counters = await actorManager.counters
}
actor ActorManager {
var counters: [Int]
init(counters: [Int]) {
self.counters = counters
}
}
Finally, it is not impossible for a @Observable
class to conform to Sendable
, if you protect counters
with a Mutex
plus some hacking around,
import Synchronization
@Observable
final class SendableManager: Sendable {
var hasContent: Bool {
counters.withLock {
access(keyPath: \.dummy)
return !$0.isEmpty
}
}
private let counters = Mutex([Int]())
private let dummy: () = ()
func lastNumber() -> Int? {
counters.withLock {
access(keyPath: \.dummy)
return $0.last
}
}
func addCounter() {
counters.withLock { counters in
withMutation(keyPath: \.dummy) {
let number = Int.random(in: 1...50 )
counters.append(number)
}
}
}
}
Note that there is a dummy
property. This is used to record reads and writes to the counter
property - the counters
property cannot be directly passed to observation-related methods like access
and withMutation
because it is not Copyable
.
Upvotes: 0