Cannot call non isolated method defined on the protocol in Swift 6.0 - Stack Overflow

We're trying to adapt Swift Strict Concurrency in Swift 6.0 and encountered a problem with quite c

We're trying to adapt Swift Strict Concurrency in Swift 6.0 and encountered a problem with quite common use case in our code where we're trying to fetch something using the injected service that is behind a protocol.

Code below results in the following error:

Sending main actor-isolated 'self.service' to nonisolated instance method 'fetch()' risks causing data races between nonisolated and main actor-isolated uses

I think I understand this issue but I'm wondering what's the best way to approach it.

We've got many services defined like that in the code base. Since they rely on fetching data using HTTP Requests or Core Data

protocol ServiceProtocol {
    func fetch() async -> Data
}

final class Service: ServiceProtocol {
    func fetch() async -> Data { Data() }
}

@MainActor
final class TestViewModel {
    private let service: ServiceProtocol

    init(service: ServiceProtocol = Service()) {
        self.service = service
    }

    func fetchData() async {
        let data = await service.fetch()
        // TODO: Do something with data
    }
}

class ViewController: UIViewController {
    let viewModel = TestViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()

        Task {
            await viewModel.fetchData()
        }
    }
}

Since these services don't store any state (or at least most of them) the most sensible way is to mark these protocols as Sendable. Does it make sense or are there any alternative approaches?

We're trying to adapt Swift Strict Concurrency in Swift 6.0 and encountered a problem with quite common use case in our code where we're trying to fetch something using the injected service that is behind a protocol.

Code below results in the following error:

Sending main actor-isolated 'self.service' to nonisolated instance method 'fetch()' risks causing data races between nonisolated and main actor-isolated uses

I think I understand this issue but I'm wondering what's the best way to approach it.

We've got many services defined like that in the code base. Since they rely on fetching data using HTTP Requests or Core Data

protocol ServiceProtocol {
    func fetch() async -> Data
}

final class Service: ServiceProtocol {
    func fetch() async -> Data { Data() }
}

@MainActor
final class TestViewModel {
    private let service: ServiceProtocol

    init(service: ServiceProtocol = Service()) {
        self.service = service
    }

    func fetchData() async {
        let data = await service.fetch()
        // TODO: Do something with data
    }
}

class ViewController: UIViewController {
    let viewModel = TestViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()

        Task {
            await viewModel.fetchData()
        }
    }
}

Since these services don't store any state (or at least most of them) the most sensible way is to mark these protocols as Sendable. Does it make sense or are there any alternative approaches?

Share Improve this question edited 16 hours ago Joakim Danielson 52.2k5 gold badges33 silver badges71 bronze badges asked 17 hours ago mikro098mikro098 2,3532 gold badges35 silver badges52 bronze badges 2
  • Services should be actor – lorem ipsum Commented 16 hours ago
  • Making the ServiceProtocol conform to Sendable or annotating it with @MainActor, or making the service an actor would solve the concurrency issue. However, it would not solve any potential race conditions. Consider what happens, when fetchData() is called multiple times before it returns. That is, you would still end up with a wonky solution. In order to actually solve the issue thoroughly, you need to add more logic into your TestViewModel that tracks a more fine grade state, like knowing that an async function is pending and that another call to fetchData() shall have no effect. – CouchDeveloper Commented 10 hours ago
Add a comment  | 

3 Answers 3

Reset to default 0

A simple solution for the code you've actually shown is to mark both ServiceProtocol and Service as @MainActor. That's what I do in the exactly parallel situation in my own apps.

You can make the ServiceProtocol conform to Sendable:

protocol ServiceProtocol: Sendable {
    func fetch() async -> Data
}

Then, for Service to conform to ServiceProtocol it could either:

  1. Be a final class without any mutable state;

  2. Conform to some global actor (whether @MainActor or your own custom global actor);

  3. If it’s going to be a non-isolated class with a mutable state, you would have to implement your own synchronization (which is generally inadvisable as you generally end up writing a bunch of manual synchronization code).

Or, if the service was going to be an actor, we might define the protocol accordingly:

protocol ServiceProtocol: Actor {
    func fetch() async -> Data
}

actor Service: ServiceProtocol {
    func fetch() async -> Data {…}
}

I would lean towards the latter, but all of them will get the job done.

It's ok if remove MainActor and add [viewModel]

protocol ServiceProtocol {
    func fetch() async -> Data
}

final class Service: ServiceProtocol {
    func fetch() async -> Data { Data() }
}

final class TestViewModel {
    private let service: ServiceProtocol

    init(service: ServiceProtocol = Service()) {
        self.service = service
    }

    func fetchData() async {
        let data = await service.fetch()
        // TODO: Do something with data
    }
}

class ViewController: UIViewController {
    let viewModel = TestViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()

        Task { [viewModel] in
            await viewModel.fetchData()
        }
    }
}

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1743759036a4502282.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信