[英]Swift mutable structs in closure of class and struct behave differently
我有一个类(A),它有一个struct变量(S)。 在这个类的一个函数中,我在struct变量上调用一个mutating函数,这个函数需要一个闭包。 此闭包的主体检查struct变量的name属性。
结构的变异函数依次调用某个类(B)的函数。 这个类的函数再次关闭。 在这个闭包的主体中改变结构,即更改name属性,并调用第一个类提供的闭包。
当我们检查struct的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()
为什么这种行为不同。 我认为区别因素应该是我是使用viewModel的结构还是类。 但这取决于Networking是一个类还是结构,它独立于任何ViewController或ViewModel。 任何人都可以帮我理解这个吗?
我想我对原始问题中的行为有所了解。 我的理解来自闭包内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参数的闭包是转义还是非转义类型。 如果它是非逃避的,那么在外面可以看到变化,如果它是逃避则不是。
所以回到我们原来的例子。 这是流程:
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 // users:%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在评论中指出的那样,“如果你在使用networkingClass = NetworkingClass()之后添加这行self.data =”D“并删除'self.data =”C“'那么它会打印'D'”。 这也是有道理的,因为闭包之外的self是由viewController保持的精确自我,因为你在闭包内删除了self.data =“C”,没有捕获viewModel self。 另一方面,如果你不删除self.data =“C”,那么它会捕获一份self。 在这种情况下,print语句打印C.检查。
这解释了changeFromClass的行为,但是正确运行的changeFromStruct呢? 理论上,应该将相同的逻辑应用于changeFromStruct,而事情不应该起作用。 但事实证明(通过为changeFromStruct函数发出SIL),在networkingStruct.fetchDataOverNetwork函数中捕获的viewModel自身值与闭包外部相同,因此修改了相同的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 self)将被确实调用,因此不需要执行alloc_box并进行单独的复制。
真实场景解决方案
实际上,fetchDataOverNetwork将发出Web服务请求并返回。 当响应到来时,将调用完成。 所以它总是逃避类型。 这将产生同样的问题。 一些丑陋的解决方案可能是:
使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) } }
希望我的解释清楚。 我知道这很令人困惑,所以你必须多次阅读并试一试。
最后一个是3.0中被接受的关于消除这种混淆的快速提议。 我不确定这是否是在swift 3.0中实现的。
发出SIL的步骤:
将所有代码放在swift文件中。
转到终端并执行此操作:
swiftc -emit-sil StructsInClosure.swift> output.txt
查看output.txt,搜索您想要查看的方法。
这个怎么样?
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中分配新的View Model。 所以我想通过使用Protocol Extension自动创建它。 didSet观察者运作良好! 但这需要在委托方法中删除强制转换。
这不是一个解决方案,但是通过这段代码,我们可以看到ViewController's
viewModel.data
是为类和struct情况正确设置的。 所不同的是, viewModel.changeFromClass
关闭捕捉陈旧self.viewModel.data
。 特别注意,只有类的'3 self'打印是错误的。 不是'2 self'和'4 self'打印包装它。
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()
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.