reza23
reza23

Reputation: 3385

Simple Sendable issue with Swift6

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

Answers (1)

Sweeper
Sweeper

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

Related Questions