Быстрые изменяемые структуры при закрытии класса и структуры ведут себя по-разному

У меня есть класс (A), который имеет переменную структуры (S). В одной функции этого класса я вызываю мутирующую функцию над структурной переменной, эта функция принимает замыкание. Тело этого замыкания проверяет свойство имени переменной структуры.

Мутирующая функция Struct по очереди вызывает функцию некоторого класса (B). Функция этого класса снова занимает замыкание. В этом замыкании измените структуру, то есть измените свойство name, и вызовите замыкание, предоставленное первым классом.

Когда вызывается замыкание первого класса (A), когда мы проверяем свойство name структуры, оно никогда не изменяется.

Но на шаге 2, если я использую struct (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 или класса. Но здесь это зависит от того, является ли сеть классом или структурой, которая не зависит от любого ViewController или ViewModel. Может кто-нибудь помочь мне понять это?

3 ответа

Я думаю, что у меня есть представление о поведении, которое мы получаем в первоначальном вопросе. Мое понимание основано на поведении входных параметров внутри замыканий.

Короткий ответ:

Это связано с тем, является ли замыкание, которое захватывает типы значений, экранирующим или неэкранирующим. Чтобы этот код работал, сделайте это.

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. Мы все знаем это поведение, когда используем ссылочные типы. Но для типов значений входные параметры передаются по значению. Таким образом, здесь 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.changeFromClass для структуры viewModel, self является ссылкой на экземпляр класса viewController, поэтому он такой же, как и мы, созданные с помощью var c = ViewController(), Так же, как с.
  2. В мутации ViewModel

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

    мы создаем экземпляр класса Networking и передаем замыкание функции fetchDataOverNetwork. Здесь обратите внимание, что для функции changeFromClass замыкание, которое принимает fetchDataOverNetwork, имеет экранирующий тип, потому что changeFromClass не предполагает, что закрытие, переданное в fetchDataOverNetwork, будет вызвано или нет до возврата changeFromClass.

  3. Self viewModel, захваченный в закрытии fetchDataOverNetwork, на самом деле является копией self viewModel. Так что self.data = "C" фактически изменяет копию viewModel, а не тот экземпляр, который хранится в viewController.

  4. Вы можете проверить это, если поместите весь код в файл swift и создадите SIL (Swift Intermediate Language). Шаги для этого в конце этого ответа. Становится ясно, что захват собственной модели viewModel в закрытии fetchDataOverNetwork предотвращает оптимизацию собственной модели viewModel для использования в стеке. Это означает, что вместо использования alloc_stack, собственная переменная viewModel выделяется с помощью alloc_box:

    % 3 = alloc_box $ ViewModelStruct, var, name "self", argno 2 // пользователи: %4, %11, %13, %16, %17

  5. Когда мы печатаем self.viewModel.data в замыкании changeFromClass, он печатает данные viewModel, которые хранятся в viewController, а не копию, которая изменяется замыканием fetchDataOverNetwork. А поскольку замыкание fetchDataOverNetwork имеет экранирующий тип и данные viewModel используются (печатаются) до того, как функция changeFromClass может вернуться, измененный viewModel не копируется в исходный viewModel (viewController's).

  6. Теперь, как только метод changeFromClass вернет измененный viewModel, он будет скопирован обратно в исходный viewModel, поэтому, если вы выполните "print(self.viewModel.data)" сразу после вызова changeFromClass, вы увидите, что значение изменилось. (это потому, что, хотя предполагается, что fetchDataOverNetwork имеет экранирующий тип, во время выполнения он фактически оказывается неэскейпинговым)

Теперь, как @san указал в комментариях, что "если вы добавите эту строку self.data = "D"после того, как пусть networkClass = NetworkingClass() и удалите self.data =" C "", то она напечатает "D" ". Это также имеет смысл, потому что self вне замыкания является точным self, которое удерживается viewController, так как вы удалили self.data = "C" внутри замыкания, захват viewModel не происходит. С другой стороны, если вы не удалите self.data = "C", тогда он захватывает копию себя. В этом случае оператор print печатает C. Проверьте это.

Это объясняет поведение changeFromClass, но как насчет changeFromStruct, который работает правильно? Теоретически такая же логика должна применяться к changeFromStruct, и все не должно работать. Но, как выясняется (путем передачи SIL для функции changeFromStruct) собственное значение viewModel, захваченное в networkStruct.fetchDataOverNetwork, является тем же self, что и вне замыкания, поэтому везде изменяется тот же self viewModel:

debug_value_addr% 1: $ * ViewModelStruct, var, имя "self", argno 2 // id: %2

Это сбивает с толку, и у меня нет объяснения этому. Но это то, что я нашел. По крайней мере, это проясняет ситуацию с изменением поведения класса.

Демо-код Решение:

Для этого демонстрационного кода решение, которое заставит changeFromClass работать так, как мы ожидаем, состоит в том, чтобы сделать закрытие функции fetchDataOverNetwork неэкранирующим, например, так:

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

Это говорит функции changeFromClass, что перед возвратом переданное закрытие (то есть запись selfMedm viewModel) будет вызвано наверняка, поэтому нет необходимости делать alloc_box и делать отдельную копию.

Реальный сценарий Решения:

В действительности fetchDataOverNetwork сделает запрос и возврат веб-службы. Когда приходит ответ, будет вызвано завершение. Так что это всегда будет экранирующим типом. Это создаст ту же проблему. Некоторые уродливые решения для этого могут быть:

  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 структурой. Объявите переменную замыкания в структуре и вызывайте ее самостоятельно с каждой мутирующей функцией. Абонент предоставит тело этого закрытия.

    /// 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. Поместите весь свой код в быстрый файл.

  2. Перейти к терминалу и сделать это:

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

  3. Посмотрите на output.txt, найдите методы, которые вы хотите увидеть.

Как насчет этого?

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://stackru.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. Поэтому я хочу сделать это автоматически, используя расширение протокола. Обозреватель didSet работает хорошо! Но для этого нужно убрать принудительное приведение в методе делегата.

Это не решение, но с помощью этого кода мы видим, что ViewController's, viewModel.data правильно настроен как для классов, так и для структур. Что отличается тем, что viewModel.changeFromClass закрытие захватывает несвежую self.viewModel.data, В частности, обратите внимание, что только печать "3 self" для класса неверна. Не "2 я" и "4 я" печатные издания, обертывающие это.

введите описание изображения здесь

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()
Другие вопросы по тегам