Why/when Swift 中是否允许 EnvironmentObjects 使用未初始化的非可选值?

Why/when are uninitialized non-optional values allowed in Swift for EnvironmentObjects?

在 Swift 中,这会在 运行 时间崩溃:

class EmptyData: BindableObject {
    let didChange = PassthroughSubject<EmptyData, Never>()
}

struct RandomView : View {
    @EnvironmentObject var emptyData: EmptyData
    @EnvironmentObject var emptyData2: EmptyData

    var body: some View {
        Text("Hello World!")
    }
}

并在 SceneDelegate.swift 中:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    let window = UIWindow(frame: UIScreen.main.bounds)
    // The emptyData variables are not initialized as seen below
    window.rootViewController = UIHostingController(rootView: RandomView())
    self.window = window
    window.makeKeyAndVisible()
}

Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)

解决这个问题并不难,但很奇怪:

window.rootViewController = UIHostingController(rootView: RandomView().environmentObject(EmptyData()))

那么这里发生了什么?我通过 EmptyData() 和 SwiftUI 决定 emptyDataemptyData2 应该使用相同的对象引用进行初始化?我还可以传递其他甚至不存在的环境对象作为 RandomView 实例中的变量:

window.rootViewController = UIHostingController(rootView: RandomView().environmentObject(EmptyData()).environmentObject(SomeData()))

和SwiftUI很高兴运行,虽然SomeData()没有在RandomView()的实例中的任何地方使用并且应该触发编译时间我认为错误。

为什么在编译时允许未初始化的值在初始化对象时没有初始化它们,为什么我们可以自由地传递环境实例而不对它们做任何事情?在我看来有点像 Javascript,我喜欢 Swift 中的强静态安全输入...我不明白为什么成员智能初始化器只生成一个采用环境变量的初始化器作为参数。

EnvironmentObject 属性 委托有一个不带参数的 init() 方法,它为包装属性提供隐式初始化

@EnvironmentObject var emptyData: EmptyData
@EnvironmentObject var emptyData2: EmptyData

(这在 Modern Swift API Design 视频中大致在 28:10 处进行了解释)。这就是为什么这些 (non-optional) 属性不需要(明确的)初始值。

文档还指出 EnvironmentObject 是(强调)

... a dynamic view property that uses a bindable object supplied by an ancestor view to invalidate the current view whenever the bindable object changes.

You must set a model object on an ancestor view by calling its environmentObject(_:) method.

所以我是这样理解的:

  • 如果在当前视图或其祖先之一的环境中找到匹配的可绑定对象(在您的情况下:EmptyData 的实例),则属性将初始化为该对象。
  • 如果在祖先视图中找不到匹配的可绑定对象,则程序会因运行时错误而终止。
  • 环境对象可用于视图层次结构中的全部、部分或none 视图。 (请参阅 29:20 处的 Data Flow Through SwiftUI。)因此,提供未在 RandomView 中使用的环境对象(在您的情况下:SomeData 的实例)不是错误.

什么是@EnvironmentObject?

A linked View property that reads a BindableObject supplied by an ancestor

因此,环境道具可以从 祖先 提供给子级,不一定应该来自其直接父级。 有了这个,看看下面的代码片段,因为 RandomViewGrandParent 将所需的 Env 对象注入到环境中,如果 RandomViewParent 的子级需要相同的 Env obj,RandomViewParent 不需要做任何事情。 RandomViewParent 可以只启动视图而无需再次传递 env obj。

class EmptyData: BindableObject {
    let didChange = PassthroughSubject<EmptyData, Never>()
}

struct RandomViewGrandParent : View {
    var body: some View {
        RandomViewParent().environmentObject(EmptyData())
    }
}

struct RandomViewParent : View {
    @EnvironmentObject var emptyData: EmptyData
    @EnvironmentObject var emptyData2: EmptyData

    var body: some View {
        RandomView()
    }
}

struct RandomView : View {
    @EnvironmentObject var emptyData: EmptyData
    @EnvironmentObject var emptyData2: EmptyData

    var body: some View {
        Text("Hello World!")
    }
}

然后回答你的另一个问题 -

我传递了 EmptyData() 并且 SwiftUI 决定 emptyData 和 emptyData2 应该用相同的对象引用初始化吗?

那是因为 EnvironmentObject 符合 BindableObject 并且 BindableObject 的 didChange 是一个 Publisher,所以我相信它认为 emptyData 和 emptyData2 想要订阅相同的 events/values 因此两者使用相同的 ref。