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

时间:2016-06-16 06:15:22

标签: ios swift mvvm mutable swift-structs

我有一个具有结构变量(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。任何人都可以帮我理解这个吗?

3 个答案:

答案 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参数的闭包是转义还是非转义类型。如果它是非逃避的,那么外面就会看到变化,如果它是逃避的话,那就不会发生变化。

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

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

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

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

  3. 在其中捕获的viewModel self fetchDataOverNetwork的闭包实际上是viewModel self的副本。 所以self.data =" C"实际上是在改变viewModel的副本 viewController保存的同一个实例。

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

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

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

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

    1. 使ViewModel成为一个非结构类。这确保了viewModel 自我是一个参考,到处都是一样的。但是我不喜欢它 关于MVVM的互联网上的所有示例代码都使用了viewModel类。 在我看来,iOS应用程序的主要代码将是ViewController, ViewModel和Models,如果所有这些都是类,那么你真的 没有使用价值类型。
    2. 使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)
          }
      }
      
    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)
          }
      }
      
    4. 希望我的解释清楚。我知道这很令人困惑,所以你必须多次阅读和尝试。

      我提到的一些资源是hereherehere

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

      发出SIL的步骤:

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

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

          

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

      3. 查看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'sviewModel.data已正确设置为类和结构案例。不同的是viewModel.changeFromClass闭包捕获陈旧的self.viewModel.data。特别注意只有&#39; 3自我&#39;打印课程是错误的。不是&#39; 2自我&#39;和&#39; 4自我&#39;打印包装它。

enter image description here

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