Swift mutable structs in closure of class and struct behave differently
我有一个类(A),它有一个结构变量。在这个类的一个函数中,我在结构变量上调用了一个可变函数,这个函数接受一个闭包。此闭包的主体检查结构变量的name属性。
结构的可变函数轮流调用某个类(B)的函数。这个类的函数再次得到一个闭包。在这个闭包的主体中,改变结构,即更改name属性,并调用第一个类提供的闭包。
当我们在检查结构的name属性的位置调用第一个类(A)闭包时,它永远不会更改。
但在步骤2中,如果我使用结构(C)而不是类B,我会看到类A的闭包结构内部实际上发生了变化。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | 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() |
为什么会有这种不同的行为。我认为区分的因素应该是我是对ViewModel使用结构,还是对类使用结构。但在这里,它取决于网络是一个类还是一个结构,独立于任何ViewController或ViewModel。有人能帮我理解吗?
这个怎么样?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | 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() |
在解决方案2、3中,它需要在ViewController中分配新的视图模型。所以我想通过使用协议扩展来自动实现它。我的观察员工作得很好!但这需要消除委托方法中的强制强制强制转换。
我想我对我们在最初问题中的行为有一个想法。我的理解源于闭包内输入输出参数的行为。好的。
简短回答:好的。
它与捕获值类型的闭包是转义还是非转义有关。要使此代码起作用,请执行此操作。好的。
1 2 3 4 5 6 | class NetworkingClass { func fetchDataOverNetwork(@nonescaping completion:()->()) { // Fetch Data from netwrok and finally call the closure completion() } } |
长回答:好的。
让我先介绍一下上下文。好的。
inout参数用于更改函数范围之外的值,如下代码所示:好的。
1 2 3 4 5 6 7 | func changeOutsideValue(inout x: Int) { closure = {x} closure() } var x = 22 changeOutsideValue(&x) print(x) // => 23 |
这里x作为inout参数传递给函数。此函数在闭包中更改x的值,因此它在其范围之外进行更改。现在x的值是23。当我们使用引用类型时,我们都知道这种行为。但对于值类型,输入参数是传递值。这里x是函数中的传递值,标记为inout。在将x传递到此函数之前,将创建并传递x的副本。所以在changeOutsideValue内部,这个拷贝被修改了,而不是原来的x。现在当这个函数返回时,这个修改过的x拷贝被复制回原来的x。所以我们看到x只在函数返回时被修改了。实际上,如果在更改了inout参数之后,函数是否返回,即捕获x的闭包是转义类型还是非转义类型。好的。
当闭包是转义类型时,即它只捕获复制的值,但在函数返回之前不调用它。请看下面的代码:好的。
1 2 3 4 5 6 7 8 9 | 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参数的闭包是转义类型还是非转义类型。如果它是不扩散的,那么变化会出现在外部,如果它是在逃避,那么它们不会。好的。
回到我们原来的例子。这就是流程:好的。
在ViewModel的变化中好的。
1 | func changeFromClass(completion:()->()) |
我们创建了一个网络类实例并将闭包传递给fetchdataovernetwork函数。通知这里是changeFromClass函数的闭包,fetchdataovernetwork takes是转义类型,因为ChangeFromClass不假设已传入关闭在ChangeFromClass之前是否调用FetchDataOverNetwork返回。好的。
在fetchdataovernetwork的闭包实际上是viewmodel self的一个副本。因此self.data="c"实际上正在更改ViewModel的副本,而不是由ViewController保存的同一实例。好的。
如果将所有代码放在一个swift文件中并发出SIL,就可以验证这一点。(斯威夫特中间语言)。步骤到此结束回答。很明显,在fetchdataovernetwork关闭会阻止viewModel self已优化以堆叠。这意味着不用alloc_堆栈,使用alloc_框分配viewModel自变量:好的。
%3 = alloc_box $ViewModelStruct, var, name"self", argno 2 // users:
%4,
%11, %13, %16, %17Ok.
当我们在changeFromClass闭包中打印self.viewModel.data时,它将打印由viewController保存的viewModel数据,而不是由fetchdataovernetwork闭包更改的副本。而且由于fetchdataovernetwork闭包是转义类型,并且在changefromclass函数返回之前使用(打印)了viewModel的数据,因此更改后的viewModel不会复制到原始的viewModel(viewController)。好的。
现在,只要ChangeFromClass方法返回已更改的ViewModel,并将其复制回原始的ViewModel,那么如果在ChangeFromClass调用后执行"打印(self.viewModel.data)",则会看到值已更改。(这是因为尽管假定fetchdataovernetwork是转义类型,但在运行时它实际上是非转义类型)好的。
正如@san在评论中指出的那样,"如果在let networkingclass=networkingclass()之后添加这行self.data="d",然后删除"self.data="c",那么它将打印"d"。这也很有意义,因为闭包外的self是由viewController保持的确切self,因为您删除了self.data="c"闭包内的self,所以不捕获viewModel self。另一方面,如果不删除self.data="c",则它会捕获self的副本。在这种情况下,打印对账单打印C。检查一下。好的。
这解释了changefromclass的行为,但是changefromstruct工作正常呢?理论上,同样的逻辑应该应用于changefromstruct,而事物不应该工作。但事实证明(通过为changefromstruct函数发出SIL),在networkstruct.fetchdataovernetwork函数中捕获的viewModel自身值与闭包外部的自身值相同,因此在任何地方都会修改相同的viewModel自身:好的。
debug_value_addr %1 : $*ViewModelStruct, var, name"self", argno 2 //
id: %2Ok.
这让人困惑,对此我没有任何解释。但这就是我发现的。至少它可以让人们从阶级行为的变化中解脱出来。好的。
演示代码解决方案:好的。
对于此演示代码,使changeFromClass工作的解决方案是使fetchdataovernetwork函数的闭包不泄漏,如下所示:好的。
1 2 3 4 5 6 | class NetworkingClass { func fetchDataOverNetwork(@nonescaping completion:()->()) { // Fetch Data from netwrok and finally call the closure completion() } } |
这告诉changefromclass函数,在返回传递的闭包(即捕获viewmodel self)之前,它将被确定地调用,因此不需要执行alloc_Box并进行单独的复制。好的。
真实场景解决方案:好的。
实际上,fetchdataovernetwork会发出Web服务请求并返回。当响应到来时,将调用完成。所以它总是属于转义类型。这将创建相同的问题。解决这个问题的一些丑陋的办法可能是:好的。
使ViewModel成为结构。从mutating函数返回一个新的mutated自我,作为回报值或内部完成取决于您用例:好的。
1 2 3 4 5 6 7 8 9 | /// ViewModelStruct mutating func changeFromClass(completion:(ViewModelStruct)->()){ let networkingClass = NetworkingClass() networkingClass.fetchDataOverNetwork { self.data ="C" self = ViewModelStruct(self.data) completion(self) } } |
在这种情况下,调用方必须始终确保将返回的值分配给它的原始实例,如下所示:好的。
1 2 3 4 5 6 7 | /// ViewController func changeViewModelStruct() { viewModel.changeFromClass { changedViewModel in self.viewModel = changedViewModel print(self.viewModel.data) } } |
使ViewModel成为结构。在结构中声明一个闭包变量,并从每个可变函数中用self调用它。调用方将提供此闭包的主体。好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | /// 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
Ok.
查看output.txt,搜索要查看的方法。好的。
好啊。
这不是一个解决方案,但是通过这段代码,我们可以看到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | class NetworkingClass { func fetchDataOverNetwork(completion:()->()) { // Fetch Data from netwrok and finally call the closure print(" class: \(self)") completion() } } struct NetworkingStruct { func fetchDataOverNetwork(completion:()->()) { // Fetch Data from netwrok and finally call the closure print(" struct: \(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() |