我想我对我们在原始问题中的行为有所了解。我的理解来源于闭包内的 inout 参数的行为。
简短的回答:
这与捕获值类型的闭包是转义还是非转义有关。要使此代码正常工作,请执行此操作。
class NetworkingClass {
func fetchDataOverNetwork(@nonescaping completion:()->()) {
// Fetch Data from netwrok and finally call the closure
completion()
}
}
长答案:
让我先给出一些背景。
inout 参数用于更改函数范围之外的值,如下面的代码所示:
func changeOutsideValue(inout x: Int) {
closure = {x}
closure()
}
var x = 22
changeOutsideValue(&x)
print(x) // => 23
这里 x 作为 inout 参数传递给函数。这个函数在闭包中改变 x 的值,所以它在它的范围之外被改变。现在 x 的值为 23。当我们使用引用类型时,我们都知道这种行为。但是对于值类型,inout 参数是按值传递的。所以这里 x 是函数中的值传递,并标记为 inout。在将 x 传递给此函数之前,会创建并传递 x 的副本。所以在changeOutsideValue里面这个副本被修改了,而不是原来的x。现在,当这个函数返回时,这个 x 的修改副本复制回原始 x。所以我们看到 x 只有在函数返回时才在外部被修改。实际上,它看到如果在更改 inout 参数之后函数是否返回,即捕获 x 的闭包是转义类型还是非转义类型。
当闭包是转义类型时,即它只是捕获复制的值,但在函数返回之前它不会被调用。看下面的代码:
func changeOutsideValue(inout x: Int)->() -> () {
closure = {x}
return closure
}
var x = 22
let c= changeOutsideValue(&x)
print(x) // => 22
c()
print(x) // => 22
这里的函数在转义闭包中捕获 x 的副本以供将来使用并返回该闭包。因此,当函数返回时,它会将 x 的未更改副本写回 x(值为 22)。如果打印 x,它仍然是 22。如果调用返回的闭包,它会更改闭包内部的本地副本,并且永远不会复制到 x 外部,因此外部 x 仍然是 22。
因此,这完全取决于您更改 inout 参数的闭包是转义类型还是非转义类型。如果它是非逃逸的,则可以在外面看到变化,如果它是逃逸的,它们就不会。
所以回到我们最初的例子。这是流程:
- ViewController 在 viewModel 结构体上调用 viewModel.changeFromClass 函数,self 是 viewController 类实例的引用,所以它和我们使用 创建的 self 一样
var c = ViewController()
,所以和 c 一样。
在 ViewModel 的变异中
func changeFromClass(completion:()->())
我们创建一个 Networking 类实例并将一个闭包传递给 fetchDataOverNetwork 函数。请注意,对于 changeFromClass 函数,fetchDataOverNetwork 采用的闭包是转义类型,因为 changeFromClass 不假设在 changeFromClass 返回之前将调用或不调用在 fetchDataOverNetwork 中传递的闭包。
在 fetchDataOverNetwork 的闭包中捕获的 viewModel self 实际上是 viewModel self 的副本。所以 self.data = "C" 实际上是在改变 viewModel 的副本,而不是 viewController 持有的同一个实例。
如果您将所有代码放在一个 swift 文件中并发出 SIL(Swift 中间语言),您可以验证这一点。对此的步骤在此答案的末尾。很明显,在 fetchDataOverNetwork 闭包中捕获 viewModel self 会阻止 viewModel self 被优化到堆栈。这意味着不是使用 alloc_stack,而是使用 alloc_box 分配 viewModel 自变量:
%3 = alloc_box $ViewModelStruct, var, name "self", argno 2 // 用户: %4, %11, %13, %16, %17
当我们在 changeFromClass 闭包中打印 self.viewModel.data 时,它打印的是 viewController 持有的 viewModel 的数据,而不是被 fetchDataOverNetwork 闭包更改的副本。并且由于 fetchDataOverNetwork 闭包是转义类型,并且在 changeFromClass 函数返回之前使用(打印)了 viewModel 的数据,因此更改后的 viewModel 不会复制到原始 viewModel(viewController 的)。
现在只要 changeFromClass 方法返回更改后的 viewModel 就会被复制回原来的 viewModel,所以如果你在 changeFromClass 调用之后执行“print(self.viewModel.data)”,你会看到值发生了变化。(这是因为虽然 fetchDataOverNetwork 被假定为转义类型,但在运行时它实际上是非转义类型)
现在正如@san 在评论中指出的那样,“如果你在 let networkingClass = NetworkingClass() 之后添加这一行 self.data = "D" 并删除 'self.data = "C" ',那么它会打印 'D'"。这也是有道理的,因为闭包外的 self 正是 viewController 持有的 self,因为您在闭包内删除了 self.data = "C",所以没有捕获 viewModel self。另一方面,如果您不删除 self.data = "C" 那么它会捕获 self 的副本。在这种情况下,打印语句打印 C。检查。
这解释了 changeFromClass 的行为,但是正常工作的 changeFromStruct 呢?理论上,应该将相同的逻辑应用于 changeFromStruct 并且事情不应该起作用。但事实证明(通过为 changeFromStruct 函数发出 SIL)networkStruct.fetchDataOverNetwork 函数中捕获的 viewModel self 值与闭包之外的 self 相同,因此在任何地方都修改了相同的 viewModel self :
debug_value_addr %1 : $*ViewModelStruct, var, name "self", argno 2 // id: %2
这令人困惑,我对此没有任何解释。但这就是我发现的。至少它清除了关于 changefromClass 行为的空气。
演示代码解决方案:
对于这个演示代码,让 changeFromClass 像我们期望的那样工作的解决方案是让 fetchDataOverNetwork 函数的闭包不转义,如下所示:
class NetworkingClass {
func fetchDataOverNetwork(@nonescaping completion:()->()) {
// Fetch Data from netwrok and finally call the closure
completion()
}
}
这告诉 changeFromClass 函数,在它返回传递的闭包(即捕获 viewModel 自身)之前,肯定会调用它,因此无需执行 alloc_box 并制作单独的副本。
真实场景解决方案:
实际上 fetchDataOverNetwork 将发出 Web 服务请求并返回。当响应到来时,将调用完成。所以它将始终是转义类型。这将产生同样的问题。一些丑陋的解决方案可能是:
- 使 ViewModel 成为类而不是结构。这确保 viewModel self 是一个引用并且在任何地方都是相同的。但我不喜欢它,尽管互联网上所有关于 MVVM 的示例代码都使用 viewModel 类。在我看来,iOS 应用程序的主要代码将是 ViewController、ViewModel 和 Models,如果所有这些都是类,那么你真的不使用值类型。
使 ViewModel 成为一个结构。从变异函数返回一个新的变异自我,根据您的用例作为返回值或内部完成:
/// ViewModelStruct
mutating func changeFromClass(completion:(ViewModelStruct)->()){
let networkingClass = NetworkingClass()
networkingClass.fetchDataOverNetwork {
self.data = "C"
self = ViewModelStruct(self.data)
completion(self)
}
}
在这种情况下,调用者必须始终确保将返回值分配给它的原始实例,如下所示:
/// ViewController
func changeViewModelStruct() {
viewModel.changeFromClass { changedViewModel in
self.viewModel = changedViewModel
print(self.viewModel.data)
}
}
使 ViewModel 成为一个结构。在 struct 中声明一个闭包变量,并在每个变异函数中使用 self 调用它。调用者将提供此闭包的主体。
/// ViewModelStruct
var viewModelChanged: ((ViewModelStruct) -> Void)?
mutating func changeFromClass(completion:()->()) {
let networkingClass = NetworkingClass()
networkingClass.fetchDataOverNetwork {
self.data = "C"
viewModelChanged(self)
completion(self)
}
}
/// ViewController
func viewDidLoad() {
viewModel = ViewModelStruct()
viewModel.viewModelChanged = { changedViewModel in
self.viewModel = changedViewModel
}
}
func changeViewModelStruct() {
viewModel.changeFromClass {
print(self.viewModel.data)
}
}
希望我的解释清楚。我知道这很令人困惑,因此您必须多次阅读和尝试。
我提到的一些资源在这里、这里和这里。
最后一个是在 3.0 中接受的关于消除这种混淆的快速提案。我不确定这是否在 swift 3.0 中实现。
发出 SIL 的步骤:
将所有代码放在一个 swift 文件中。
转到终端并执行以下操作:
swiftc -emit-sil StructsInClosure.swift > output.txt
查看output.txt,搜索你想看的方法。