Swift mutable structs in closure of class and struct behave differently

后端 未结 3 934
攒了一身酷
攒了一身酷 2021-02-05 18:02

I have a class(A) that has a struct variable (S). In one function of this class I call a mutating function on struct variable,this function takes a closure. Body of this closure

相关标签:
3条回答
  • 2021-02-05 18:42

    How about this?

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

    In your solution 2, 3, it need to assign new View Model in ViewController. So I wanna make it automatically by using Protocol Extension. didSet observer works well! But this need to remove force casting in delegate method.

    0 讨论(0)
  • 2021-02-05 18:44

    This is not a solution but with this code we can see that the ViewController's, viewModel.data is properly set for both class and struct cases. What is different is that the viewModel.changeFromClass closure captures a stale self.viewModel.data. Notice in particular that only the '3 self' print for class is wrong. Not the '2 self' and '4 self' prints wrapping it.

    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()
    
    0 讨论(0)
  • 2021-02-05 19:00

    I think I have an idea about the behaviour we are getting in the original question. My understanding is derived from the behaviour of inout parameters inside closures.

    Short answer:

    It is related to whether closure that captures value types are escaping or nonescaping. To make this code work do this.

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

    Long answer:

    Let me give some context first.

    inout parameters are used to change values outside of the function scope, like in the below code:

    func changeOutsideValue(inout x: Int) {
      closure = {x}
      closure()
    }
    var x = 22
    changeOutsideValue(&x)
    print(x) // => 23
    

    Here x is passed as inout parameter to a function. This function changes value of x in a closure, so it is changed outside of it's scope. Now x's value is 23. We all know this behaviour when we use reference types. But for value types inout parameters are pass by value. So here x is pass by value in the function, and marked as inout. Before passing x into this function, a copy of x is created and passed. So inside the changeOutsideValue this copy is modified, not the original x. Now when this function returns, this modified copy of x copied back to the original x. So we see x is modified outside only when the function returns. Actually it sees that if after changing the inout parameter if the function returns or not i.e. the closure that is capturing x is escaping kind or nonescaping kind.

    When the closure is of escaping type, i.e. it just capture the copied value, but before the function returns it is not called. Look at the below code:

    func changeOutsideValue(inout x: Int)->() -> () {
      closure = {x}
      return closure
    }
    var x = 22
    let c= changeOutsideValue(&x)
    print(x) // => 22
    c()
    print(x) // => 22
    

    Here function is capturing the copy of x in a escaping closure for future usages and returns that closure. So when function returns it writes the unchanged copy of x back to x (value is 22). If you print x, it is still 22. If you call the returned closure, it changes the local copy inside closure and it is never copied to outside x, so outside x is still 22.

    So it all depends whether the closure where you are changing the inout parameter is of escaping or non escaping type. If it is nonescaping, changes are seen outside, if it is escaping they are not.

    So coming back to our original example. This is the flow:

    1. ViewController calls viewModel.changeFromClass function on viewModel struct, self is the reference of the viewController class instance, so it is the same self as we created using var c = ViewController(), So it same as c.
    2. In ViewModel's mutating

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

      we create a Networking class instance and pass a closure to fetchDataOverNetwork function. Notice here that for changeFromClass function the closure that fetchDataOverNetwork takes is of escaping type, because changeFromClass makes no assumption that the closure passed in fetchDataOverNetwork will be called or not before changeFromClass returns.

    3. The viewModel self that is captured inside the fetchDataOverNetwork's closure is actually a copy of viewModel self. So self.data = "C" is actually changing the copy of viewModel, not the same instance that is hold by the viewController.

    4. You can verify this if you put all the code inside a swift file and emit SIL (Swift Intermediate Language). Steps for this are at the end of this answer. It becomes clear that capturing viewModel self in fetchDataOverNetwork closure prevents viewModel self from being optimized to stack. This means that instead of using alloc_stack, the viewModel self variable is allocated using alloc_box:

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

    5. When we print self.viewModel.data in changeFromClass closure it is printing the viewModel's data that is hold by the viewController, not the copy that is being changed by the fetchDataOverNetwork closure. And since the fetchDataOverNetwork closure is of escaping type and viewModel's data is used (printed) before changeFromClass function could return, changed viewModel is not copied to original viewModel (viewController's).

    6. Now as soon as changeFromClass method returns changed viewModel is copied back to the original viewModel, so if you do "print(self.viewModel.data)" just after changeFromClass call, you see the value is changed. (this is because though fetchDataOverNetwork is assumed to be of escaping type, at runtime it is actually turn out to be of nonescaping type)

    Now as @san pointed out in comments that "If you add this line self.data = "D" after let networkingClass = NetworkingClass() and remove 'self.data = "C" ' then it prints 'D'". This also make sense because self outside the closure is exact self that is hold by the viewController, since you removed self.data = "C" inside closure, there is no capturing of viewModel self. On the other hand if you don't remove self.data = "C" then it captures a copy of self. In this case print statement prints C. Check that.

    This explains the behaviour of changeFromClass, but what about changeFromStruct that is working properly? In theory same logic should be applied to changeFromStruct and things should not work. But as it turns out (by emitting SIL for changeFromStruct function) the viewModel self value captured in networkingStruct.fetchDataOverNetwork function is same self as outside of the closure, so everywhere the same viewModel self is modified:

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

    This is confusing and I have no explanation to this. But this is what I found. At least it clears the air about changefromClass behaviour.

    Demo code Solution:

    For this demo code the solution to make changeFromClass work as we expect is to make fetchDataOverNetwork function's closure nonescaping like so:

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

    This tells changeFromClass function that before it returns passed closure (that is capturing viewModel self) will be called for sure so there is no need to do alloc_box and make a separate copy.

    Real scenario Solutions:

    In reality fetchDataOverNetwork will make a web service request and return. When response comes then the completion will be called. So it will always be of escaping type. This will create the same issue. Some ugly solutions for this could be:

    1. Make ViewModel a class not struct. This makes sure that viewModel self is a reference and same everywhere. But I don't like it, though all the sample code on internet about MVVM uses class for viewModel. In my opinion major code of an iOS app will be ViewController, ViewModel and Models and if all of these are classes then you really doesn't use value types.
    2. Make ViewModel a struct. From mutating function return a new mutated self, either as return value or inside completion depending on your use case:

      /// ViewModelStruct
      mutating func changeFromClass(completion:(ViewModelStruct)->()){
      let networkingClass = NetworkingClass()
      networkingClass.fetchDataOverNetwork {
        self.data = "C"
        self = ViewModelStruct(self.data)
        completion(self)
      }
      }
      

      In this case caller have to always make sure it assigns the returned value to it's original instance, like so:

      /// ViewController
      func changeViewModelStruct() {
          viewModel.changeFromClass { changedViewModel in
            self.viewModel = changedViewModel
            print(self.viewModel.data)
          }
      }
      
    3. Make ViewModel a struct. Declare a closure variable in struct and call it with self from every mutating function. Caller will provide the body of this closure.

      /// 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)
          }
      }
      

    Hope I am clear in my explaination. I know it is confusing, so you will have to read and try this multiple times.

    Some of the resources I referred are here, here and here.

    Last one is a accepted swift proposal in 3.0 about removing this confusion. I am not sure if this is it is implemented in swift 3.0 or not.

    Steps to emit SIL:

    1. Put all your code in a swift file.

    2. Go to terminal and do this:

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

    3. Look at output.txt, search for methods you want to see.

    0 讨论(0)
提交回复
热议问题