如何使用@MainActor 初始化全局变量?

How do I initialize a global variable with @MainActor?

我想要某种使用 @MainActor 同步的全局变量。

这是一个示例结构:

@MainActor
struct Foo {}

我想要一个像这样的全局变量:

let foo = Foo()

但是,这不会编译并且 Call to main actor-isolated initializer 'init()' in a synchronous nonisolated context 出错。

很公平。我试过像这样在主线程上构建它:

let foo = DispatchQueue.main.sync {
    Foo()
}

编译成功!但是,它因 EXC_BAD_INSTRUCTION 而崩溃,因为 DispatchQueue.main.sync 不能在主线程上 运行。

我还尝试创建一个包装函数,例如:

func syncMain<T>(_ closure: () -> T) -> T {
    if Thread.isMainThread {
        return closure()
    } else {
        return DispatchQueue.main.sync(execute: closure)
    }
}

并使用

let foo = syncMain {
    Foo()
}

但编译器无法识别 if Thread.isMainThread 并再次抛出相同的错误消息,Call to main actor-isolated initializer 'init()' in a synchronous nonisolated context

正确的做法是什么?我需要某种可以在应用程序启动前初始化的全局变量。

一种方法是将变量存储在容器中(如 enum 作为抽象命名空间)并将其与主要参与者隔离。

@MainActor
enum Globals {
    static let foo = Foo()
}

一个同样有效的方法是在对象本身上有一个“类单例”static 属性,它具有相同的目的,但没有额外的对象。

@MainActor
struct Foo {
    static let global = Foo()
}

您现在可以通过 Foo.global 访问全局对象。

需要注意的一件事是现在将延迟初始化(在第一次调用时)而不是立即初始化。 但是,您可以通过对该对象进行任何访问来提前强制初始化。

// somewhere early on
_ = Foo.global

Swift 5.5 和 Swift 5.6 中的错误

TL;DR:@MainActor 有时不会在主线程上调用 static let 变量。请改用 static var

虽然这可以编译并工作,但似乎这可能会在主线程之外调用初始化程序,然后在初始化程序内部进行任何调用。

@MainActor
struct Bar {
    init()  {
        print("bar init is main", Thread.isMainThread)
    }
    func barCall() {
        print("bar call is main", Thread.isMainThread)
    }
}


@MainActor
struct Foo {
    @MainActor
    static let global = Foo()
    
    init() {
        print("foo init is main", Thread.isMainThread)
        let b = Bar()
        b.barCall()
    }
    
    func fooCall() {
        print("foo call is main", Thread.isMainThread)
    }
}
Task.detached {
    await Foo.global.fooCall()
}
// prints:
// foo init is main false
// bar init is main false
// bar call is main false
// foo call is main true

这是一个错误(参见 SR-16009)。

一种解决方法是始终确保初始化发生在 @MainActor 上下文中,我们可以通过在我们第一次调用的地方注释闭包来做到这一点。

Task.detached { @MainActor in
    await Foo.global.fooCall()
}

或者,您可以使用 static var 而不是 static let,因为在这种情况下会强制执行正确的隔离行为。