Actor 隔离 @Observable 类型

Rob*_*Rob 10 ios swift observableobject ios17 observation-framework

回到 WWDC 2021\xe2\x80\x99s Discover concurrency in SwiftUI,他们建议您将ObservableObject对象与主要参与者隔离。例如:

\n
struct ContentView: View {\n    @StateObject var viewModel = ViewModel()\n\n    var body: some View {\n        Text("\\(viewModel.count)")\n        .task {\n            try? await viewModel.start()\n        }\n    }\n}\n\n@MainActor \nclass ViewModel: ObservableObject {\n    var count = 0\n\n    func start() async throws {\n        while count < 10 {\n            count += 1\n            try await Task.sleep(for: .seconds(1))\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

但在 iOS 17\xe2\x80\x99s观察框架(如 WWDC 2023\xe2\x80\x99s Discover Observation in SwiftUI中介绍)中,似乎不再需要隔离到主要参与者来防止在后台线程。例如,以下内容不会出现有关从后台启动 UI 更新的警告:

\n
struct ContentView: View {\n    var viewModel = ViewModel()             // was `@StateObject var viewModel = ViewModel()`\n\n    var body: some View {\n        Text("\\(viewModel.count)")\n        .task {\n            try? await viewModel.start()\n        }\n    }\n}\n\n@Observable class ViewModel {               // was `@MainActor class ViewModel: ObservableObject {\xe2\x80\xa6}`\n    var count = 0                           // was `@Published`\n\n    func start() async throws {\n        while count < 10 {\n            count += 1\n            try await Task.sleep(for: .seconds(1))\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

目前尚不清楚消除主要参与者隔离的底层机制是什么,但它确实有效。

\n

但是,如果您ViewModel出于不从后台更新 UI 以外的原因希望将 Actor 隔离,该怎么办?例如,也许我只是想避免这个@Observable对象中的竞争?SE-0395表示它(尚)不支持可观察actor类型:

\n
\n

未来增强的另一个重点领域是对可观察actor类型的支持。这需要对参与者当前不存在的关键路径进行特定处理。

\n
\n

但是,如果一个class演员与某个全局演员(例如主要演员)隔离呢?看来我可以将视图模型与主要参与者隔离,但随后我收到错误View

\n
\n

在同步非隔离上下文中调用主参与者隔离初始化程序“init()”

\n
\n

View我也可以通过将 与主要演员隔离来解决这个错误。例如,以下内容似乎有效:

\n
@MainActor\nstruct ContentView: View {\n    var viewModel = ViewModel()\n\n    var body: some View {\n        Text("\\(viewModel.count)")\n        .task {\n            try? await viewModel.start()\n        }\n    }\n}\n\n@MainActor\n@Observable\nclass ViewModel {\n    var count = 0\n\n    func start() async throws {\n        while count < 10 {\n            count += 1\n            try await Task.sleep(for: .seconds(1))\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

但将整体View与主要演员隔离开来感觉是错误的,因为苹果显然选择不这样做(出于我无法理解的原因)。那么,简而言之,如何将类型隔离@Observable到全局参与者(例如主要参与者)?

\n

Cou*_*per 5

我只有一个解决这个问题的方法,它可能不适用于所有情况。

\n

但首先,问题是:

\n

给定一个使用并初始化模型的SwiftUI 视图:

\n
struct ContentView: View {\n    @State var viewModel = ViewModel()\n\n    var body: some View {\n        ...\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

以及相应的模型,它使用 @MainActor 来同步其成员:

\n
@MainActor\n@Observable\nclass ViewModel {\n    var count: Int = 0\n    \n    func foo() async throws {\n        // asynchronously mutates member `count` which \n        // needs to be synchronised. Here, through \n        // using `@MainActor`. That way, it\'s guaranteed \n        // that mutations on `count` happen solely on \n        // the main thread.\n        ...\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

当尝试编译时,我们在 struct 中收到错误ContentenView

\n
    @State var viewModel = ViewModel() <== Call to main actor-isolated initializer \'init()\' in a synchronous nonisolated context\n
Run Code Online (Sandbox Code Playgroud)\n

也就是说,编译器要确保 的初始化器Model将在主线程上调用。虽然我们直观地假设,无论如何都会出现这种情况,但它毕竟是一个视图,但编译器需要明确的事实。

\n

这一要求的原因并不那么明显。通常,我们在其他语言中保证了线程安全,其中在通过其他方式访问成员时在任何线程上调用构造函数。

\n

对于 Swift,我们可以阅读更多相关信息\n On Actors and Initialization, SE-0327,特别是:\n overly-restrictive-non-async-initializers

\n

将 SwiftUI 视图与主要参与者关联是一种解决方案,但今天可能会导致其他问题。

\n

另一个解决方案可能只是声明初始化程序非隔离- 但要小心 \xe2\x80\x93 它可能会破坏同步。在这种情况下,可以通过显式声明初始化程序为空主体的非隔离来工作:

\n
@MainActor\n@Observable\nclass ViewModel {\n    var count: Int = 0\n    \n    nonisolated init() {}\n    \n    func start() async throws {\n        while count < 10 {\n            count += 1\n            try await Task.sleep(for: .seconds(1))\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

笔记:

\n

为了使用空的非隔离初始化程序,所有成员都必须在声明时进行初始化。例如:

\n
class ViewModel {\n    var count: Int = 0\n    ...\n
Run Code Online (Sandbox Code Playgroud)\n

非隔离初始化程序无法初始化/设置成员。如果我们尝试,我们会得到错误:

\n
\n

主要参与者隔离属性“count”无法从非隔离上下文中发生突变

\n
\n

警告

\n

声明为非隔离的更复杂的初始化程序可能容易出现数据争用!请仔细阅读以上链接。

\n

这是当前问题的解决方法。我希望,这些事情将来能得到更多完成。

\n