在closures类和结构的Swift可变结构行为有所不同

我有一个类(A)有一个结构variables(S)。 在这个类的一个函数中,我调用了structvariables的变异函数,这个函数需要一个闭包。 这个闭包的主体检查结构variables的名称属性。

Struct的变异函数依次调用某个类(B)的函数。 这个类的函数再次closures。 在这个闭包的主体中改变结构,即改变name属性,并调用第一个类提供的闭包。

当第一个类(A)闭包被调用时,我们正在检查结构的名称属性,它永远不会改变。

但是在步骤2中,如果我使用结构(C)而不是类B,则会发现类A的闭包结构实际上已更改。 以下是代码:

class NetworkingClass { func fetchDataOverNetwork(completion:()->()) { // Fetch Data from netwrok and finally call the closure completion() } } struct NetworkingStruct { func fetchDataOverNetwork(completion:()->()) { // Fetch Data from netwrok and finally call the closure completion() } } struct ViewModelStruct { /// Initial value var data: String = "A" /// Mutate itself in a closure called from a struct mutating func changeFromStruct(completion:()->()) { let networkingStruct = NetworkingStruct() networkingStruct.fetchDataOverNetwork { self.data = "B" completion() } } /// Mutate itself in a closure called from a class mutating func changeFromClass(completion:()->()) { let networkingClass = NetworkingClass() networkingClass.fetchDataOverNetwork { self.data = "C" completion() } } } class ViewController { var viewModel: ViewModelStruct = ViewModelStruct() func changeViewModelStruct() { print(viewModel.data) /// This never changes self.viewModel inside closure, Why Not? viewModel.changeFromClass { print(self.viewModel.data) } /// This changes self.viewModel inside/outside closure, Why? viewModel.changeFromStruct { print(self.viewModel.data) } } } var c = ViewController() c.changeViewModelStruct() 

为什么这个不同的行为 我认为区分因素应该是我是否使用视图模型或类的结构。 但是这取决于Networking是一个类还是结构,它独立于任何ViewController或ViewModel。 任何人都可以帮我理解这个吗?

我想我对于原来问题中的行为有一个概念。 我的理解来自封闭内部inout参数的行为。

简短的回答:

它涉及到捕获值types的闭包是逃避还是不逃逸。 为了使这个代码工作,做到这一点。

 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作为inoutparameter passing给一个函数。 这个函数改变了闭包中x的值,所以它在它的作用域之外被改变了。 现在x的值是23.当我们使用引用types时,我们都知道这个行为。 但是对于数值typesinout参数是按值传递的。 所以这里x是在函数中传递的值,并标记为inout。 在将x传入此函数之前,会创build并传递x的副本。 所以在changeOutsideValue里面修改这个副本,而不是原来的x。 现在,当这个函数返回时,这个x的修改副本被复制回原来的x。 所以我们看到x只在函数返回时才被修改。 实际上如果函数返回或者没有更改inout参数后就会看到正在捕获的x的闭包是逃逸types。

当闭包是逃避types,即它只是捕获复制的值,但在函数返回之前,它不会被调用。 看下面的代码:

 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参数的封闭是逃避还是不逃避的types。 如果它不是转义的话,那么在外面就可以看到变化,如果它们没有转义的话。

所以回到我们最初的例子。 这是stream量:

  1. ViewController在viewModel结构体上调用viewModel.changeFromClass函数,self是viewController类实例的引用,所以和我们使用var c = ViewController()创build的一样,所以它和c一样。
  2. 在ViewModel的变异

     func changeFromClass(completion:()->()) 

    我们创build一个Networking类实例,并将一个闭包传递给fetchDataOverNetwork函数。 请注意,对于changeFromClass函数,fetchDataOverNetwork采用的闭包是转义types,因为changeFromClass不会假定在fetchDataOverNetwork中传递的闭包将在changeFromClass返回之前被调用。

  3. 在fetchDataOverNetwork闭包中捕获的viewModel self实际上是viewModel self的一个副本。 所以self.data =“C”实际上是改变viewModel的副本,而不是由viewController持有的同一个实例。

  4. 你可以validation这个,如果你把所有的代码在一个快速的文件,并发出SIL(迅速中间语言)。 这个步骤是在这个答案的结尾。 很明显,在fetchDataOverNetwork闭包中捕获viewModel self会阻止viewModel self被优化为堆栈。 这意味着,而不是使用alloc_stack,viewModel自variables是使用alloc_box分配的:

    %3 = alloc_box $ ViewModelStruct,var,名称为“self”,argno 2 //用户:%4,%11,%13,%16,%17

  5. 当我们在changeFromClass闭包中打印self.viewModel.data时,它将打印由viewController保存的viewModel的数据,而不是由fetchDataOverNetwork闭包修改的副本。 而且,由于fetchDataOverNetwork闭包是转义types,并且在changeFromClass函数可以返回之前使用viewModel的数据(打印),所以已更改的viewModel不会复制到原始viewModel(viewController's)。

  6. 现在只要changeFromClass方法返回改变viewModel被复制回原来的viewModel,所以如果你在“print(self.viewModel.data)”中执行changeFromClass调用,你会看到值被改变。 (这是因为尽pipefetchDataOverNetwork被认为是转义types,但在运行时却实际上是非转义types的)

现在,@san在注释中指出:“如果在让networkingClass = NetworkingClass()并将”self.data =“C”“删除后添加此行self.data =”D“,则会打印出'D'”。 这也是有道理的,因为在闭包之外的自我是由viewController保存的确切的自我,因为你在闭包中删除了self.data =“C”,所以没有捕获viewModel self。 另一方面,如果你不删除self.data =“C”,那么它会捕获自己的副本。 在这种情况下,打印语句打印C.检查。

这解释了changeFromClass的行为,但changeFromStruct是否正常工作呢? 从理论上讲,同样的逻辑应该适用于从结构变化,事情不应该工作。 但事实certificate(通过发送SIL来获取changeFromStruct函数),networkingStruct.fetchDataOverNetwork函数中捕获的viewModel自身值与闭包之外的自身相同,所以在同一个viewModel self中处处被修改:

debug_value_addr%1:$ * ViewModelStruct,var,名称为“self”,argno 2 // id:%2

这很混乱,我对此没有任何解释。 但这是我发现的。 至less它清除了关于changefromClass行为的空气。

演示代码解答:

对于这个演示代码,使changeFromClass工作的解决scheme如我们所期望的那样使fetchDataOverNetwork函数的闭包非像这样:

 class NetworkingClass { func fetchDataOverNetwork(@nonescaping completion:()->()) { // Fetch Data from netwrok and finally call the closure completion() } } 

这告诉changeFromClass函数,它返回之前传递闭包(这是捕获viewModel自我)将被肯定调用,所以没有必要做alloc_box并做一个单独的副本。

真实场景解决scheme

实际上,fetchDataOverNetwork将发出Web服务请求并返回。 当回应来完成将被称为。 所以它将永远是逃避types。 这会造成同样的问题。 一些丑陋的解决scheme可能是:

  1. 使ViewModel不是结构的类。 这可以确保viewModel self是一个参考,并在任何地方。 但是我不喜欢它,尽pipe互联网上关于MVVM的所有示例代码都使用了viewModel的类。 在我看来,一个iOS应用程序的主要代码将是ViewController,ViewModel和Models,如果所有这些都是类,那么你真的不使用值types。
  2. 使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) } } 
  3. 使ViewModel成为一个结构。 在struct中声明一个闭包variables,并从每个变异函数中用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版本中接受的关于消除这种混淆的迅速build议。 我不确定这是否是在3.0中实现的。

发出SIL的步骤:

  1. 把所有的代码放在一个快速的文件中。

  2. 去terminal,并做到这一点:

    swiftc -emit-sil StructsInClosure.swift> output.txt

  3. 看看output.txt,search你想看到的方法。

这个怎么样?

 import Foundation import XCPlayground protocol ViewModel { var delegate: ViewModelDelegate? { get set } } protocol ViewModelDelegate { func viewModelDidUpdated(model: ViewModel) } struct ViewModelStruct: ViewModel { var data: Int = 0 var delegate: ViewModelDelegate? init() { } mutating func fetchData() { XCPlaygroundPage.currentPage.needsIndefiniteExecution = true NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: "http://stackoverflow.com")!) { result in self.data = 20 self.delegate?.viewModelDidUpdated(self) print("viewModel.data in fetchResponse : \(self.data)") XCPlaygroundPage.currentPage.finishExecution() }.resume() } } protocol ViewModeling { associatedtype Type var viewModel: Type { get } } typealias ViewModelProvide = protocol<ViewModeling, ViewModelDelegate> class ViewController: ViewModelProvide { var viewModel = ViewModelStruct() { didSet { viewModel.delegate = self print("ViewModel in didSet \(viewModel)") } } func viewDidLoad() { viewModel = ViewModelStruct() } func changeViewModelStruct() { print(viewModel) viewModel.fetchData() } } extension ViewModelDelegate where Self: ViewController { func viewModelDidUpdated(viewModel: ViewModel) { self.viewModel = viewModel as! ViewModelStruct } } var c = ViewController() c.viewDidLoad() c.changeViewModelStruct() 

在你的解决scheme2,3中,它需要在ViewController中分配新的视图模型。 所以我想通过使用协议扩展自动完成。 didSet观察者运作良好! 但是这需要在委托方法中删除强制转换。

这不是一个解决scheme,但是通过这段代码,我们可以看到ViewController's viewModel.data被正确地设置为类和结构的情况。 有什么不同的是, viewModel.changeFromClass闭包捕获一个陈旧的viewModel.changeFromClass 。 特别要注意的是,只有class级的“三自”印刷是错误的。 不是包装它的“2自我”和“4自我”印刷品。

在这里输入图像说明

 class NetworkingClass { func fetchDataOverNetwork(completion:()->()) { // Fetch Data from netwrok and finally call the closure print("\nclass: \(self)") completion() } } struct NetworkingStruct { func fetchDataOverNetwork(completion:()->()) { // Fetch Data from netwrok and finally call the closure print("\nstruct: \(self)") completion() } } struct ViewModelStruct { /// Initial value var data: String = "A" /// Mutate itself in a closure called from a struct mutating func changeFromStruct(completion:()->()) { let networkingStruct = NetworkingStruct() networkingStruct.fetchDataOverNetwork { print("1 \(self)") self.data = "B" print("2 \(self)") completion() print("4 \(self)") } } /// Mutate itself in a closure called from a class mutating func changeFromClass(completion:()->()) { let networkingClass = NetworkingClass() networkingClass.fetchDataOverNetwork { print("1 \(self)") self.data = "C" print("2 \(self)") completion() print("4 \(self)") } } } class ViewController { var viewModel: ViewModelStruct = ViewModelStruct() func changeViewModelStruct() { print(viewModel.data) /// This never changes self.viewModel, Why Not? viewModel.changeFromClass { print("3 \(self.viewModel)") print(self.viewModel.data) } /// This changes self.viewModel, Why? viewModel.changeFromStruct { print("3 \(self.viewModel)") print(self.viewModel.data) } } } var c = ViewController() c.changeViewModelStruct()