问题描述
问题截图和报错代码行:
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/