我有一个具有结构变量(S)的类(A)。在这个类的一个函数中,我在struct变量上调用一个mutating函数,这个函数需要一个闭包。这个闭包的主体检查struct变量的name属性。
struct的变异函数依次调用某个类(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。任何人都可以帮我理解这个吗?
答案 0 :(得分:2)
我想我对原始问题中的行为有所了解。我的理解来自闭包内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 回报。
在其中捕获的viewModel self fetchDataOverNetwork的闭包实际上是viewModel self的副本。 所以self.data =" C"实际上是在改变viewModel的副本 viewController保存的同一个实例。
如果将所有代码放在swift文件中并发出SIL,则可以验证这一点 (斯威夫特中级语言)。这方面的步骤就是结束 回答。很明显,捕获viewModel self fetchDataOverNetwork闭包阻止了viewModel self 优化堆栈。这意味着不使用alloc_stack, viewModel自变量使用alloc_box分配:
%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' s)。
现在,只要changeFromClass方法返回已更改的viewModel,就会将其复制回原始的viewModel,因此如果执行" print(self.viewModel.data)"在changeFromClass调用之后,您会看到值已更改。 (这是因为虽然假设fetchDataOverNetwork属于转义类型,但在运行时实际上它实际上是非转义类型)
现在@san在评论中指出"如果你添加这一行self.data =" D"之后让networkClass = NetworkingClass()并删除' self.data =" C" '然后打印' D'"。这也是有道理的,因为闭包之外的自我是由viewController保持的精确自我,因为你删除了self.data =" C"在内部闭包中,没有捕获viewModel self。另一方面,如果你不删除self.data =" C"然后它捕获了一份自我。在这种情况下,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成为一个结构。从变异函数返回一个新的变异 自我,作为返回值或内部完成取决于您的 用例:
/// 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,搜索您想要查看的方法。
答案 1 :(得分:2)
这个怎么样?
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观察者运作良好!但这需要在委托方法中删除强制转换。
答案 2 :(得分:0)
这不是解决方案,但使用此代码,我们可以看到ViewController's
,viewModel.data
已正确设置为类和结构案例。不同的是viewModel.changeFromClass
闭包捕获陈旧的self.viewModel.data
。特别注意只有&#39; 3自我&#39;打印课程是错误的。不是&#39; 2自我&#39;和&#39; 4自我&#39;打印包装它。
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()