关于ios:类和结构闭包中的Swift可变结构表现不同

Swift mutable structs in closure of class and struct behave differently

我有一个类(A),它有一个结构变量。在这个类的一个函数中,我在结构变量上调用了一个可变函数,这个函数接受一个闭包。此闭包的主体检查结构变量的name属性。

结构的可变函数轮流调用某个类(B)的函数。这个类的函数再次得到一个闭包。在这个闭包的主体中,改变结构,即更改name属性,并调用第一个类提供的闭包。

当我们在检查结构的name属性的位置调用第一个类(A)闭包时,它永远不会更改。

但在步骤2中,如果我使用结构(C)而不是类B,我会看到类A的闭包结构内部实际上发生了变化。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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使用结构,还是对类使用结构。但在这里,它取决于网络是一个类还是一个结构,独立于任何ViewController或ViewModel。有人能帮我理解吗?


这个怎么样?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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中分配新的视图模型。所以我想通过使用协议扩展来自动实现它。我的观察员工作得很好!但这需要消除委托方法中的强制强制强制转换。


我想我对我们在最初问题中的行为有一个想法。我的理解源于闭包内输入输出参数的行为。好的。

简短回答:好的。

它与捕获值类型的闭包是转义还是非转义有关。要使此代码起作用,请执行此操作。好的。

1
2
3
4
5
6
class NetworkingClass {
  func fetchDataOverNetwork(@nonescaping completion:()->()) {
    // Fetch Data from netwrok and finally call the closure
    completion()
  }
}

长回答:好的。

让我先介绍一下上下文。好的。

inout参数用于更改函数范围之外的值,如下代码所示:好的。

1
2
3
4
5
6
7
func changeOutsideValue(inout x: Int) {
  closure = {x}
  closure()
}
var x = 22
changeOutsideValue(&x)
print(x) // => 23

这里x作为inout参数传递给函数。此函数在闭包中更改x的值,因此它在其范围之外进行更改。现在x的值是23。当我们使用引用类型时,我们都知道这种行为。但对于值类型,输入参数是传递值。这里x是函数中的传递值,标记为inout。在将x传递到此函数之前,将创建并传递x的副本。所以在changeOutsideValue内部,这个拷贝被修改了,而不是原来的x。现在当这个函数返回时,这个修改过的x拷贝被复制回原来的x。所以我们看到x只在函数返回时被修改了。实际上,如果在更改了inout参数之后,函数是否返回,即捕获x的闭包是转义类型还是非转义类型。好的。

当闭包是转义类型时,即它只捕获复制的值,但在函数返回之前不调用它。请看下面的代码:好的。

1
2
3
4
5
6
7
8
9
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对ViewModel调用ViewModel.ChangeFromClass函数struct,self是viewcontroller类实例的引用,所以它和我们用var c = ViewController()创建的一样,所以和C一样。
  • 在ViewModel的变化中好的。

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

    我们创建了一个网络类实例并将闭包传递给fetchdataovernetwork函数。通知这里是changeFromClass函数的闭包,fetchdataovernetwork takes是转义类型,因为ChangeFromClass不假设已传入关闭在ChangeFromClass之前是否调用FetchDataOverNetwork返回。好的。

  • 在fetchdataovernetwork的闭包实际上是viewmodel self的一个副本。因此self.data="c"实际上正在更改ViewModel的副本,而不是由ViewController保存的同一实例。好的。

  • 如果将所有代码放在一个swift文件中并发出SIL,就可以验证这一点。(斯威夫特中间语言)。步骤到此结束回答。很明显,在fetchdataovernetwork关闭会阻止viewModel self已优化以堆叠。这意味着不用alloc_堆栈,使用alloc_框分配viewModel自变量:好的。

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

    Ok.

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

  • 现在,只要ChangeFromClass方法返回已更改的ViewModel,并将其复制回原始的ViewModel,那么如果在ChangeFromClass调用后执行"打印(self.viewModel.data)",则会看到值已更改。(这是因为尽管假定fetchdataovernetwork是转义类型,但在运行时它实际上是非转义类型)好的。

  • 正如@san在评论中指出的那样,"如果在let networkingclass=networkingclass()之后添加这行self.data="d",然后删除"self.data="c",那么它将打印"d"。这也很有意义,因为闭包外的self是由viewController保持的确切self,因为您删除了self.data="c"闭包内的self,所以不捕获viewModel self。另一方面,如果不删除self.data="c",则它会捕获self的副本。在这种情况下,打印对账单打印C。检查一下。好的。

    这解释了changefromclass的行为,但是changefromstruct工作正常呢?理论上,同样的逻辑应该应用于changefromstruct,而事物不应该工作。但事实证明(通过为changefromstruct函数发出SIL),在networkstruct.fetchdataovernetwork函数中捕获的viewModel自身值与闭包外部的自身值相同,因此在任何地方都会修改相同的viewModel自身:好的。

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

    Ok.

    这让人困惑,对此我没有任何解释。但这就是我发现的。至少它可以让人们从阶级行为的变化中解脱出来。好的。

    演示代码解决方案:好的。

    对于此演示代码,使changeFromClass工作的解决方案是使fetchdataovernetwork函数的闭包不泄漏,如下所示:好的。

    1
    2
    3
    4
    5
    6
    class NetworkingClass {
      func fetchDataOverNetwork(@nonescaping completion:()->()) {
        // Fetch Data from netwrok and finally call the closure
        completion()
      }
    }

    这告诉changefromclass函数,在返回传递的闭包(即捕获viewmodel self)之前,它将被确定地调用,因此不需要执行alloc_Box并进行单独的复制。好的。

    真实场景解决方案:好的。

    实际上,fetchdataovernetwork会发出Web服务请求并返回。当响应到来时,将调用完成。所以它总是属于转义类型。这将创建相同的问题。解决这个问题的一些丑陋的办法可能是:好的。

  • 使ViewModel成为类而不是结构。这可以确保ViewModel自我是一个参照物,在任何地方都是一样的。但是我不喜欢Internet上有关MVVM的所有示例代码都将类用于ViewModel。我认为iOS应用程序的主要代码是viewcontroller,视图模型和模型,如果所有这些都是类,那么不使用值类型。
  • 使ViewModel成为结构。从mutating函数返回一个新的mutated自我,作为回报值或内部完成取决于您用例:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /// ViewModelStruct
    mutating func changeFromClass(completion:(ViewModelStruct)->()){
    let networkingClass = NetworkingClass()
    networkingClass.fetchDataOverNetwork {
      self.data ="C"
      self = ViewModelStruct(self.data)
      completion(self)
    }
    }

    在这种情况下,调用方必须始终确保将返回的值分配给它的原始实例,如下所示:好的。

    1
    2
    3
    4
    5
    6
    7
    /// ViewController
    func changeViewModelStruct() {
        viewModel.changeFromClass { changedViewModel in
          self.viewModel = changedViewModel
          print(self.viewModel.data)
        }
    }
  • 使ViewModel成为结构。在结构中声明一个闭包变量,并从每个可变函数中用self调用它。调用方将提供此闭包的主体。好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    /// 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

    Ok.

  • 查看output.txt,搜索要查看的方法。好的。

  • 好啊。


    这不是一个解决方案,但是通过这段代码,我们可以看到ViewController'sviewModel.data都是为类和结构案例正确设置的。不同的是,viewModel.changeFromClass闭包捕获了一个过时的self.viewModel.data。请特别注意,只有类的"3 self"打印是错误的。不是"2 self"和"4 self"在包装它。

    enter image description here

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    class NetworkingClass {
      func fetchDataOverNetwork(completion:()->()) {
        // Fetch Data from netwrok and finally call the closure
        print("
    class: \(self)")
        completion()
      }
    }

    struct NetworkingStruct {
      func fetchDataOverNetwork(completion:()->()) {
        // Fetch Data from netwrok and finally call the closure
        print("
    struct: \(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()