我想我对我们在原始问题中的行为有所了解。我的理解来源于闭包内的 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 函数
struct,self是viewController类实例的引用,
所以它与我们使用
var c = ViewController() 创建的自我相同,
所以和c一样。
-
在 ViewModel 的变异中
func changeFromClass(completion:()->())
我们创建一个网络类
实例并将闭包传递给 fetchDataOverNetwork 函数。注意
这里对于 changeFromClass 函数的闭包
fetchDataOverNetwork 采用的是转义类型,因为
changeFromClass 不假设闭包传入
fetchDataOverNetwork 是否会在 changeFromClass 之前被调用
返回。
在
fetchDataOverNetwork 的闭包实际上是 viewModel self 的一个副本。
所以 self.data = "C" 实际上是在改变 viewModel 的副本,而不是
viewController 持有的同一个实例。
-
如果您将所有代码放入 swift 文件并发出 SIL,您可以验证这一点
(Swift 中间语言)。这个步骤在这个结尾
回答。很明显,在
fetchDataOverNetwork 闭包防止 viewModel self 成为
优化堆栈。这意味着,而不是使用 alloc_stack,
viewModel 自身变量是使用 alloc_box 分配的:
%3 = alloc_box $ViewModelStruct, var, name "self", argno 2 // 用户:
%4,
%11, %13, %16, %17
当我们在 changeFromClass 闭包中打印 self.viewModel.data 时,它打印的是 viewController 持有的 viewModel 的数据,而不是被 fetchDataOverNetwork 闭包更改的副本。而且由于 fetchDataOverNetwork 闭包是转义类型,并且 viewModel 的数据在 changeFromClass 函数返回之前被使用(打印),因此更改后的 viewModel 不会复制到原始 viewModel(viewController 的)。
现在,只要 changeFromClass 方法返回更改后的 viewModel 就会被复制回原始 viewModel,因此如果您在调用 changeFromClass 之后执行“print(self.viewModel.data)”,您会看到值已更改。 (这是因为虽然 fetchDataOverNetwork 被假定为转义类型,但在运行时它实际上是非转义类型)
现在正如@san 在 cmets 中指出的那样,“如果在 let networkingClass = NetworkingClass() 之后添加这一行 self.data = "D" 并删除 'self.data = "C" ',那么它会打印 'D'" .这也是有道理的,因为闭包外的 self 正是 viewController 持有的 self,因为您在闭包内删除了 self.data = "C",所以没有捕获 viewModel self。另一方面,如果您不删除 self.data = "C" 那么它会捕获 self 的副本。在这种情况下,print 语句会打印 C。检查一下。
这解释了 changeFromClass 的行为,但是正常工作的 changeFromStruct 呢?理论上,应该将相同的逻辑应用于 changeFromStruct 并且事情不应该起作用。但事实证明(通过为 changeFromStruct 函数发出 SIL)networkStruct.fetchDataOverNetwork 函数中捕获的 viewModel self 值与闭包之外的 self 相同,因此在任何地方都修改了相同的 viewModel self:
debug_value_addr %1 : $*ViewModelStruct, var, name "self", argno 2 //
编号:%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 会发出一个网络服务请求并返回。当响应到来时,将调用完成。所以它将始终是转义类型。这将产生同样的问题。一些丑陋的解决方案可能是:
- 使 ViewModel 成为类而不是结构。这可以确保 viewModel
self 是一个参考,在任何地方都是一样的。但我不喜欢它,虽然
互联网上所有关于 MVVM 的示例代码都使用了 viewModel 的类。
在我看来,iOS 应用程序的主要代码将是 ViewController,
ViewModel 和 Models,如果所有这些都是类,那么你真的
不使用值类型。
-
使 ViewModel 成为一个结构。从变异函数返回一个新的变异
self,作为返回值或内部完成,具体取决于您的
用例:
/// 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)
}
}
希望我的解释清楚。我知道这很令人困惑,因此您必须多次阅读和尝试。
我提到的一些资源是here、here 和here。
最后一个是 3.0 中接受的关于消除这种混淆的快速提案。我不确定这是否在 swift 3.0 中实现。
发出 SIL 的步骤:
将所有代码放入一个 swift 文件中。
-
转到终端并执行以下操作:
swiftc -emit-sil StructsInClosure.swift > output.txt
查看output.txt,搜索你想看的方法。