swift 作用子分离@可观察类型

yi0zb3m4  于 2023-08-02  发布在  Swift
关注(0)|答案(1)|浏览(136)

在WWDC 2021的Discover concurrency in SwiftUI中,他们建议您将ObservableObject对象隔离到主要参与者。例如:

struct ContentView: View {
    @StateObject var viewModel = ViewModel()

    var body: some View {
        Text("\(viewModel.count)")
        .task {
            try? await viewModel.start()
        }
    }
}

@MainActor 
class ViewModel: ObservableObject {
    var count = 0

    func start() async throws {
        while count < 10 {
            count += 1
            try await Task.sleep(for: .seconds(1))
        }
    }
}

字符串
但在iOS 17的Observation框架中(如WWDC 2023的Discover Observation in SwiftUI中所介绍的),似乎不再需要隔离到主要参与者来防止UI更新在后台线程上触发。例如,以下操作在没有关于从后台启动UI更新的警告的情况下工作:

struct ContentView: View {
    var viewModel = ViewModel()             // was `@StateObject var viewModel = ViewModel()`

    var body: some View {
        Text("\(viewModel.count)")
        .task {
            try? await viewModel.start()
        }
    }
}

@Observable class ViewModel {               // was `@MainActor class ViewModel: ObservableObject {…}`
    var count = 0                           // was `@Published`

    func start() async throws {
        while count < 10 {
            count += 1
            try await Task.sleep(for: .seconds(1))
        }
    }
}


消除主要参与者隔离的基本机制并不明显,但它确实有效。
但是,如果您希望ViewModel是隔离的参与者,而不是从后台更新UI,该怎么办呢?例如,也许我只是想避免这个@Observable对象中的竞争?SE-0395说它还不支持可观察的actor类型:
未来增强的另一个重点领域是支持可观察的actor类型。这将需要对参与者当前不存在的关键路径进行特定处理。
但是,如果一个class是与某个全局参与者(比如主参与者)隔离的参与者,情况又会怎样呢?看起来我可以将视图模型与主参与者隔离开来,但随后我在View中得到了一个错误:
在同步非隔离上下文中调用主参与者隔离的初始化器“init()”
我也可以通过将View与主要参与者隔离来避免这个错误。例如,以下似乎起作用:

@MainActor
struct ContentView: View {
    var viewModel = ViewModel()

    var body: some View {
        Text("\(viewModel.count)")
        .task {
            try? await viewModel.start()
        }
    }
}

@MainActor
@Observable
class ViewModel {
    var count = 0

    func start() async throws {
        while count < 10 {
            count += 1
            try await Task.sleep(for: .seconds(1))
        }
    }
}


但是把整个View隔离到主要演员身上感觉是错误的,而苹果显然选择了不这样做(原因我不知道)。因此,简而言之,如何将@Observable类型隔离到全局参与者(例如主参与者)?

deikduxw

deikduxw1#

我只有一个解决这个问题的方法,它可能不适用于所有情况。
但首先,问题是:
给定一个SwiftUI视图,它使用并 * 初始化 * 一个Model:

struct ContentView: View {
    @State var viewModel = ViewModel()

    var body: some View {
        ...
    }
}

字符串
以及相应的Model,它使用@MainActor来同步其成员:

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


当尝试编译时,我们在struct ContentenView中得到一个错误:

@State var viewModel = ViewModel() <== Call to main actor-isolated initializer 'init()' in a synchronous nonisolated context


也就是说,编译器希望确保Model的初始化器将在主线程上被调用。虽然我们直觉地认为,无论如何都会是这种情况,但它毕竟是一个视图,编译器需要明确的事实。
这一要求的原因并不明显。通常,我们在其他语言中保证了线程安全,其中 * 构造函数 * 在任何线程上被调用,当访问成员时通过其他方式变得安全。
对于Swift,我们可以阅读更多关于On Actors and Initialization,SE-0327的内容,具体如下:过度限制非异步初始化器
将SwiftUI视图关联到主参与者上是一种解决方案,但现在可能会导致其他问题。
另一种解决方案可能只是声明初始化器 nonisolated -但要小心-它可能会破坏同步。在这种情况下,它可以通过显式声明初始化器为非隔离的空主体来工作:

@MainActor
@Observable
class ViewModel {
    var count: Int = 0
    
    nonisolated init() {}
    
    func start() async throws {
        while count < 10 {
            count += 1
            try await Task.sleep(for: .seconds(1))
        }
    }
}


注意事项:
为了使用空的非隔离初始化器,所有成员在声明时都必须初始化。举例来说:

class ViewModel {
    var count: Int = 0
    ...


非隔离初始化器不能初始化/设置成员。如果我们尝试,我们会得到错误:
主参与者隔离属性'count'不能从非隔离上下文中发生变化

注意事项

声明为非隔离的更复杂的初始化器可能容易发生数据竞争!请仔细阅读以上链接。
这是解决当前问题的一种方法。我希望,这些事情在未来得到更多的完成。

相关问题