12

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

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

当我们在检查结构的 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()

为什么会有这种不同的行为。我认为区分因素应该是我是使用视图模型的结构还是类。但这里取决于 Networking 是一个类还是一个结构,它独立于任何 ViewController 或 ViewModel。谁能帮我理解这一点?

4

3 回答 3

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 在 vi​​ewModel 结构体上调用 viewModel.changeFromClass 函数,self 是 viewController 类实例的引用,所以它和我们使用 创建的 self 一样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 // 用户: %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 在评论中指出的那样,“如果你在 let networkingClass = NetworkingClass() 之后添加这一行 self.data = "D" 并删除 'self.data = "C" ',那么它会打印 'D'"。这也是有道理的,因为闭包外的 self 正是 viewController 持有的 self,因为您在闭包内删除了 self.data = "C",所以没有捕获 viewModel self。另一方面,如果您不删除 self.data = "C" 那么它会捕获 self 的副本。在这种情况下,打印语句打印 C。检查。

这解释了 changeFromClass 的行为,但是正常工作的 changeFromStruct 呢?理论上,应该将相同的逻辑应用于 changeFromStruct 并且事情不应该起作用。但事实证明(通过为 changeFromStruct 函数发出 SIL)networkStruct.fetchDataOverNetwork 函数中捕获的 viewModel self 值与闭包之外的 self 相同,因此在任何地方都修改了相同的 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 自身)之前,肯定会调用它,因此无需执行 alloc_box 并制作单独的副本。

真实场景解决方案:

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

  1. 使 ViewModel 成为类而不是结构。这确保 viewModel self 是一个引用并且在任何地方都是相同的。但我不喜欢它,尽管互联网上所有关于 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)
        }
    }
    

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

我提到的一些资源在这里这里这里

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

发出 SIL 的步骤:

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

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

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

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

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

于 2016-06-20T08:43:38.470 回答
0

这不是一个解决方案,但通过这段代码,我们可以看到 ,ViewController'sviewModel.data类和结构情况正确设置。不同的是viewModel.changeFromClass闭包捕获了一个 stale 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()
于 2016-06-16T07:05:08.707 回答