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

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

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

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

Ответ 1

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

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

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

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, экранирует вид или тип nonescaping.

Когда закрытие имеет тип экранирования, то есть он просто захватывает скопированное значение, но перед возвратом функции он не вызывается. Посмотрите на приведенный ниже код:

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, типа escape или non escaping. Если он не отображается, изменения видны снаружи, если они ускользают, это не так.

Итак, вернемся к нашему оригинальному примеру. Это поток:

  • ViewController вызывает функцию viewModel.changeFromClass на viewModel struct, self - это ссылка экземпляра класса viewController, так что это то же самое, что мы создали с помощью var c = ViewController(), Так оно же как c.
  • В мутации ViewModel

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

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

  • Объект viewModel, который фиксируется внутри Закрытие fetchDataOverNetwork на самом деле является копией self. Поэтому self.data = "C" фактически меняет копию viewModel, а не тот же экземпляр, который удерживается viewController.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Сделать ViewModel классом not struct. Это гарантирует, что viewModel само является ссылкой и везде. Но мне это не нравится, хотя весь пример кода в Интернете о MVVM использует класс для viewModel. На мой взгляд, основным кодом приложения iOS будет ViewController, ViewModel и Models, и если все это классы, то вы действительно не использует типы значений.
  • Сделать ViewModel структурой. Из функции mutating возвратите новую мутированную self, либо как возвращаемое значение, либо внутреннее завершение в зависимости от вашего использование:

    /// 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)
        }
    }
    
  • Сделать ViewModel структурой. Объявите переменную замыкания в struct и вызовите ее с помощью self из каждой функции mutat. Caller предоставит тело этого закрытия.

    /// 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:

  • Поместите весь свой код в быстрый файл.

  • Перейдите в терминал и выполните следующее:

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

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

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

Ответ 3

Это не решение, но с этим кодом мы можем видеть, что ViewController's, viewModel.data правильно установлен как для классов, так и для структур. Другое дело, что замыкание viewModel.changeFromClass фиксирует устаревший self.viewModel.data. Обратите внимание, в частности, что только "3 сам" печатает для класса неправильно. Не печатаются "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()