闭包中的Swift可变结构和结构的行为不同

Posted

技术标签:

【中文标题】闭包中的Swift可变结构和结构的行为不同【英文标题】:Swift mutable structs in closure of class and struct behave differently 【发布时间】:2016-10-17 12:02:58 【问题描述】:

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

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

当我们检查结构的名称属性时调用第一个类 (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。谁能帮我理解这个?

【问题讨论】:

你的意思是说,在调用changeFromClass方法后不会改变viewModel.data? 它在 changeFromClass 之后(以及在 changeFromStruct 之前)对其进行了更改,但此更改在 changeFromClass 闭包中不可见。 这对我来说真是令人难以置信。我觉得 Swift 必须按照 Apple 的以下说明进行一些优化。 As an optimization, Swift may instead capture and store a copy of a value if that value is not mutated by a closure, and if the value is not mutated after the closure is created. ..... 如果您在 let networkingClass = NetworkingClass() 之后添加此行 self.data = "D" 并删除 'self.data = "C" ',那么它会打印 'D'。此外,如果您将“struct ViewModelStruct”修改为“class ViewModelStruct”,那么它会打印“C”。 【参考方案1】:

这不是一个解决方案,但通过这段代码,我们可以看到ViewController'sviewModel.data 已针对类和结构情况正确设置。不同的是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()

【讨论】:

正是我的观点。我们可以通过在 viewModel.changeFromClass 之后(在 viewModel.changeFromStruct 之前)打印 self.viewModel.data 来验证这一点,它会打印“C”。所以调用 viewModel.changeFromClass 实际上改变了 viewModel 但它在闭包内不可见。我的猜测是内部 viewModel.changeFromClass 闭包 self.viewModel 以当前状态捕获,即 ViewModelStruct(data: "A") ,这就是我们正在打印的内容。 ViewModel 的 mutating 函数正在改变对 viewController 可见但对闭包不可见的结构。 您应该考虑 fetchDataOverNetwork 函数将异步而不是同步。我异步编辑了它,类中的视图模型没有改变。1 ViewModelStruct(data: "A") 2 ViewModelStruct(data: "B") 3 ViewModelStruct(data: "A") A 4 ViewModelStruct(data: "B") @tarun_sharma 你解决了吗?我认为变异函数在函数返回后立即改变了值。因此,当使用异步函数(如通过网络获取数据)时,视图控制器中的值在改变函数后立即更改(分配了新值)。从网络功能的完成,它改变了原始值的数据,而不是一个新的值。我遇到了同样的情况,还没有找到解决方案。如果您找到解决方案,您会发布吗?这让我发疯。 :( @Paul 是的,你是对的。我有一个解决方案,您可以在完成处理程序中返回变异的结构。在这种情况下,调用者必须确保将变异的值类型分配回它的实例。另一种解决方案可能是在值类型中使用 structUpdated 闭包。这个闭包的主体是由调用者分配的,并且 struct 中的每个变异函数都必须确保它在调用完成处理程序之前调用该闭包。这些都是丑陋的解决方案,但这只是我到目前为止。 除此之外,我想我已经理解了原始行为的原因。这是在您所说的复制值类型并且在变异函数内部修改该副本的行上,并且一旦该函数返回,该变异副本就会被复制回原始值。稍后我会详细发布我的理解。【参考方案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 参数的闭包是转义类型还是非转义类型。如果是非转义,则在外部可以看到更改,如果是转义,则不会。

所以回到我们最初的例子。这是流程:

    ViewController 在 vi​​ewModel 上调用 viewModel.changeFromClass 函数 struct,self是viewController类实例的引用, 所以它与我们使用var c = ViewController() 创建的自我相同, 所以和c一样。

    在 ViewModel 的变异中

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

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

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

    如果您将所有代码放入 swift 文件并发出 SIL,您可以验证这一点 (Swift 中间语言)。这个步骤在这个结尾 回答。很明显,在 fetchDataOverNetwork 闭包防止 viewModel self 成为 优化堆栈。这意味着,而不是使用 alloc_stack, viewModel 自身变量是使用 alloc_box 分配的:

    %3 = alloc_box $ViewModelStruct, var, name "self", argno 2 // 用户: %4, %11, %13, %16, %17

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

    现在,只要 changeFromClass 方法返回更改后的 viewModel 就会被复制回原始 viewModel,因此如果您在调用 changeFromClass 之后执行“print(self.viewModel.data)”,您会看到值已更改。 (这是因为虽然 fetchDataOverNetwork 被假定为转义类型,但在运行时它实际上是非转义类型)

现在正如@san 在 cmets 中指出的那样,“如果在 let networkingClass = NetworkingClass() 之后添加这一行 self.data = "D" 并删除 'self.data = "C" ',那么它会打印 'D'" .这也是有道理的,因为闭包外的 self 正是 viewController 持有的 self,因为您在闭包内删除了 self.data = "C",所以没有捕获 viewModel self。另一方面,如果您不删除 self.data = "C" 那么它会捕获 self 的副本。在这种情况下,print 语句会打印 C。检查一下。

这解释了 changeFromClass 的行为,但是正常工作的 changeFromStruct 呢?理论上,应该将相同的逻辑应用于 changeFromStruct 并且事情不应该起作用。但事实证明(通过为 changeFromStruct 函数发出 SIL)networkStruct.fetchDataOverNetwork 函数中捕获的 viewModel self 值与闭包之外的 self 相同,因此在任何地方都修改了相同的 viewModel self:

debug_value_addr %1 : $*ViewModelStruct, var, name "self", argno 2 // 编号:%2

这令人困惑,我对此没有任何解释。但这就是我发现的。至少它清除了关于 changefromClass 行为的问题。

演示代码解决方案:

对于这个演示代码,让 changeFromClass 像我们期望的那样工作的解决方案是让 fetchDataOverNetwork 函数的闭包不转义,如下所示:

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

这告诉 changeFromClass 函数,在它返回传递的闭包(即捕获 viewModel 自身)之前,肯定会调用它,因此无需执行 alloc_box 并制作单独的副本。

真实场景解决方案:

实际上 fetchDataOverNetwork 会发出一个网络服务请求并返回。当响应到来时,将调用完成。所以它将始终是转义类型。这将产生同样的问题。一些丑陋的解决方案可能是:

    使 ViewModel 成为类而不是结构。这可以确保 viewModel self 是一个参考,在任何地方都是一样的。但我不喜欢它,虽然 互联网上所有关于 MVVM 的示例代码都使用了 viewModel 的类。 在我看来,ios 应用程序的主要代码将是 ViewController, ViewModel 和 Models,如果所有这些都是类,那么你真的 不使用值类型。

    使 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)
        
    
    

希望我的解释清楚。我知道这很令人困惑,因此您必须多次阅读和尝试。

我提到的一些资源是here、here 和here。

最后一个是 3.0 中接受的关于消除这种混淆的快速提案。我不确定这是否在 swift 3.0 中实现。

发出 SIL 的步骤:

    将所有代码放入一个 swift 文件中。

    转到终端并执行以下操作:

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

    查看output.txt,搜索你想看的方法。

【讨论】:

我在遵循 MVVM 模型时遇到了完全相同的问题。感谢您的详细解释。【参考方案3】:

这个怎么样?

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://***.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。所以我想通过使用协议扩展来自动实现它。 didSet 观察者效果很好!但这需要删除委托方法中的强制转换。

【讨论】:

以上是关于闭包中的Swift可变结构和结构的行为不同的主要内容,如果未能解决你的问题,请参考以下文章

Swift的闭包,枚举,类和结构体

在 Swift 3.0 中的转义闭包中改变自我(结构/枚举)

使用 Xcode LLDB 控制台在 Swift 中调试闭包

Swift之深入解析Sendable和@Sendable闭包代码实例

Swift之深入解析Sendable和@Sendable闭包代码实例

Swift之深入解析Sendable和@Sendable闭包代码实例