繁体   English   中英

类和结构闭包中的Swift可变结构表现不同

[英]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参数的闭包是转义还是非转义类型。 如果它是非逃避的,那么在外面可以看到变化,如果它是逃避则不是。

所以回到我们原来的例子。 这是流程:

  1. ViewController在viewModel struct上调用viewModel.changeFromClass函数,self是viewController类实例的引用,因此它与我们使用var c = ViewController()创建的相同,所以它与c相同。
  2. 在ViewModel的变异中

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

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

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

  4. 如果将所有代码放在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

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

  6. 现在,只要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服务请求并返回。 当响应到来时,将调用完成。 所以它总是逃避类型。 这将产生同样的问题。 一些丑陋的解决方案可能是:

  1. 使ViewModel成为一个不是结构的类。 这可以确保viewModel self是一个引用,并且在任何地方都是相同的。 但我不喜欢它,尽管互联网上关于MVVM的所有示例代码都使用了viewModel类。 在我看来,iOS应用程序的主要代码将是ViewController,ViewModel和Models,如果所有这些都是类,那么你真的不使用值类型。
  2. 使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) } } 
  3. 使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的步骤:

  1. 将所有代码放在swift文件中。

  2. 转到终端并执行此操作:

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

  3. 查看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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM