Xcode报错:Cannot call mutating async function ‘update()’ on actor-isolated property ‘updateER’
Xcode报错:Cannot call mutating async function ‘update()’ on actor-isolated property ‘updateER’

Xcode报错:Cannot call mutating async function ‘update()’ on actor-isolated property ‘updateER’

问题描述

问题截图和报错代码行:

Task {
    await updateER.update() // 报错代码
}

struct UpdateER {
    mutating func update() async {
        ...
    }
} 

在调取异步方法时,Xcode显示报错:

Cannot call mutating async function 'update()' on actor-isolated property 'updateER'

经排查发现,这一报错原因为:updateER 是一个 @State 属性,在 SwiftUI 中,@State 属性是视图的一部分,它们被视为 actor 隔离的。这意味着在 @State 属性上调用异步的变异方法(即改变其状态的方法)是不允许的,因为这会导致潜在的并发问题。

要解决这个问题,就需要@State 属性中提取 UpdateER 对象,并将其设为一个类对象,使用 @ObservedObject 或者将它封装在一个 @StateObject 中。

解决方案 1: 使用 @StateObject

首先,将 UpdateER 定义为一个遵循 ObservableObject 协议的类,并使用 @StateObject 来管理它。这样你就可以在视图加载时调用异步方法了。

修改 UpdateER

class UpdateER: ObservableObject {
    @Published var forexData: [String: Double] = [:]
    
    func loadForexData() {
        // 加载初始数据
    }
    
    func update() async {
        // 异步更新汇率数据
    }
}

原本UpdateER是一个结构,现在修改为一个Class类,并遵循ObservableObject协议,此外还需要将原来方法的mutating关键词删除。

Task {
    await updateER.update()   // 现在不会报错了
}

修改完成后,问题不再报错。

解决方案 2: 改用局部变量

如果你不想更改 UpdateER 的实现,可以将 updateER 改成局部变量,在 onAppear 的 Task 里面临时创建。虽然不太灵活,但可以用作一种快速的解决方法。

修改 onAppear

Task {
    var tempUpdateER = updateER
    await tempUpdateER.update()
}

这种方法是避免直接在 @State 属性上调用异步变异方法,但在大多数情况下,使用 @StateObject 会更加优雅。

使用临时变量 tempUpdateER 能解决这个问题的原因涉及到 actor 隔离和 Swift 中对值类型(如结构体)的行为。

Actor 隔离与 State 的特性

Actor 隔离:@State 属性是 actor 隔离的,这意味着它受到并发安全控制,不能直接从异步上下文中调用会修改它的函数。直接在 Task 中调用 updateER.update() 会导致编译器报错,因为编译器认为这会违反并发安全性。

如果你直接从异步上下文中调用会修改 @State 属性的函数,会破坏 SwiftUI 提供的并发安全性。因为这会导致在不同线程之间对状态的不安全访问,编译器无法保证数据一致性和正确性。

Actor 隔离意味着某些属性和方法只能通过安全的异步方式访问,以确保数据不会被多个任务同时修改。如果直接在 Task 内调用变异(mutating)函数,编译器无法保证是否安全地访问了 @State 属性,因此会产生编译错误。

换句话说,SwiftUI 中的 @State 变量可能会在多个任务和线程中被访问,因此直接修改它存在风险,Swift 编译器阻止了这种操作。

Swift 值类型行为:@State 属性(比如 updateER)是一个值类型(结构体),当你创建一个临时变量 var tempUpdateER = updateER 时,Swift 实际上是复制了 updateER 的当前状态给 tempUpdateER,生成了一个新的、独立的实例。这意味着 tempUpdateER 是一个完全独立的结构体,不再受到 @State 的 actor 隔离限制,以及不再受到 SwiftUI 并发安全的约束。

因此,在这种情况下,tempUpdateER 不再被视为 actor 隔离的属性,你可以在异步上下文中自由地对它进行修改。

具体原理

临时变量 (tempUpdateER) 是 updateER 的副本:通过 var tempUpdateER = updateER,你创建了一个独立的结构体实例,这个实例不再受到 actor 隔离的约束。

变异操作仅作用于副本:当你对 tempUpdateER 进行异步变异操作时,它只会修改 tempUpdateER 本身,而不会直接影响 updateER。

通过复制规避了并发限制:因为 tempUpdateER 是一个新的、非隔离的变量,所以你可以在 Task 中对它进行变异。

例子总结

Task {
    var tempUpdateER = updateER  // 创建一个新的副本
    await tempUpdateER.update()  // 可以异步调用,因为不受 actor 隔离限制
}

通过这种方式,你绕过了直接访问 @State 变量的 actor 隔离问题,但要注意,修改 tempUpdateER 不会影响到原本的 updateER,除非你手动将 tempUpdateER 赋值回 updateER。

注意事项

如果你需要在异步调用后保留 tempUpdateER 的变更,可以将其再赋值回去,例如:

updateER = tempUpdateER

这种模式是一种变通方法,但要确保逻辑正确,以避免在异步操作中丢失状态的更新。

相关资料

swift科普文《Actor 隔离》: https://fangjunyu.com/2024/10/22/swift%e7%a7%91%e6%99%ae%e6%96%87%e3%80%8aactor-%e9%9a%94%e7%a6%bb%e3%80%8b/

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注