Ответ 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, найдите методы, которые хотите видеть.