Я думаю, что у меня есть представление о поведении мы получаем в оригинальный вопрос. Мое понимание вытекает из поведения внутренних параметров внутри замыканий.
Короткий ответ:
Это связано ли закрытие, что улавливает типы значений убегают или nonescaping. Чтобы этот код работал, сделайте это.
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 в закрытии, поэтому изменяется вне области видимости. Теперь значение 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 (значение равно 22). Если вы печатаете x, все равно 22. Если вы вызываете возвращенное закрытие, оно меняет локальную копию внутри закрытия и никогда не копируется на внешний x, так что вне x все равно 22.
Итак, все зависит от того, закрытие, в котором вы изменяете параметр inout, имеет тип экранирования или отсутствия экранирования. Если он не отображается, изменения видны снаружи, если они ускользают, это не так.
Так что вернемся к нашему первоначальному примеру. Это поток:
- ViewController вызывает функцию viewModel.changeFromClass на ViewModel структуры, самость ссылка экземпляра класса ViewController, так это же само, как мы создали с помощью
var c = ViewController()
, Так это же, как и с.
В ViewModel-х мутирует
func changeFromClass(completion:()->())
мы создаем экземпляр класса сети и передать замыкание к функции fetchDataOverNetwork. Обратите внимание, здесь для changeFromClass функционировать замыкание, fetchDataOverNetwork занимает вытекающий тип, потому что changeFromClass не делает никаких предположений о том, что закрытие прошло в fetchDataOverNetwork будет называться или не перед changeFromClass возвращается.
ViewModel self, который зафиксирован внутри Закрытие fetchDataOverNetwork на самом деле является копией self.Model. Так что self.data = "C" фактически меняет копию viewModel, а не того же экземпляра, который удерживается viewController.
Это можно проверить, если вы поместили весь код в быстрый файл и испустили SIL (Swift Intermediate Language). Шаги для этого находятся в конце этого ответа . Становится очевидным, что захват viewModel self в fetchDataOverNetwork закрытие не позволяет viewModel самостоятельно быть оптимизирован для стека. Это означает, что вместо того, чтобы использовать alloc_stack, переменная само ViewModel выделяется с помощью alloc_box:
% 3 = alloc_box $ ViewModelStruct, вар, имя "Я", argno 2 // пользователей: % 4, % 11,% 13,% 16,% 17
Когда мы выводим self.viewModel.data в changeFromClass закрытия при печати данных в ViewModel, которая удерживается на ViewController, не копия, что в настоящее время изменен fetchDataOverNetwork закрытие. А так как закрытие fetchDataOverNetwork имеет тип экранирования, и данные viewModel используются (печатаются) до того, как функция changeFromClass может вернуться, измененный viewModel не копируется в исходный viewModel (viewController's).
Теперь, как только метод changeFromClass возвращает измененный viewModel, копируется обратно в исходный viewModel, поэтому, если вы выполните «print (self.viewModel.data)» сразу после вызова changeFromClass, вы увидите, что значение изменено. (Это потому, что хотя fetchDataOverNetwork предполагается вытекающим типа, во время выполнения он на самом деле оказывается в nonescaping типа)
Теперь, как @san отметил в комментариях, что «Если вы добавите эту строку самостоятельно .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, захваченной в функции networkingStruct.fetchDataOverNetwork такое же само, как снаружи укупорочного средства, так что везде одинаково ViewModel самостоятельно модифицируется:
debug_value_addr% 1 : $ * ViewModelStruct, вар, имя "Я", argno 2 // ID:% 2
Это сбивает с толку, и у меня нет объяснения этому. Но это то, что я нашел. По крайней мере, он очищает воздух от изменения поведения.
Демо-код Решение:
Для этого демо-коды решения сделать changeFromClass работу, как мы ожидаем, чтобы сделать замыкание функции fetchDataOverNetwork в nonescaping так:
class NetworkingClass {
func fetchDataOverNetwork(@nonescaping completion:()->()) {
// Fetch Data from netwrok and finally call the closure
completion()
}
}
Это говорит функцию changeFromClass, что прежде чем он вернет прошлое закрытие (то есть захват viewModel self), будет вызываться точно, поэтому нет необходимости делать alloc_box и делать отдельную копию.
Real Solutions Сценарий:
В действительности fetchDataOverNetwork сделает запрос на веб-службу и вернуться. Когда приходит ответ, завершение будет вызвано. Таким образом, он всегда будет иметь экранирующий тип. Это создаст ту же проблему. Некоторыми уродливыми решениями для этого могут быть:
- Сделать ViewModel классом not struct. Это гарантирует, что viewModel self является ссылкой и такой же везде. Но мне это не нравится, хотя весь пример кода в Интернете о MVVM использует класс для viewModel. На мой взгляд, основным кодом приложения iOS будет ViewController, ViewModel и Models, и если все это классы, то вы действительно не используете типы значений.
Сделать 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)
}
}
Make ViewModel a struct. Объявите переменную замыкания в 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)
}
}
Надежда Я ясно в моих объяснениях. Я знаю, что это сбивает с толку, поэтому вам придется читать и попробовать это несколько раз.
Некоторые из перечисленных мной ресурсов: here, here и here.
Последний принятый быстрый предложение в 3.0 об устранении этой путаницы. Я не уверен, что это реализовано в swift 3.0 или нет.
шаги испускать SIL:
Поместите весь код в стремительном файле.
Перейти к терминалу и сделать это:
swiftc -emit-силь StructsInClosure.swift> output.txt
Посмотрите на output.txt, поиск методов, которые вы хотите увидеть ,
Вы хотите сказать, что он не меняет viewModel.data после вызова метода changeFromClass? – iamyogish
Он меняет его после changeFromClass (и до changeFromStruct), но это изменение просто не видно внутри changeFromClass. –
Это действительно для меня. Я считаю, что Swift должен попытаться сделать некоторую оптимизацию согласно следующей заметке Apple. 'В качестве оптимизации Swift может вместо этого захватить и сохранить копию значения, если это значение не будет мутировано закрытием, и если значение не будет мутировано после создания замыкания. '..... Если вы добавите эту строку 'self.data =" D "' после 'let networkingClass = NetworkingClass()' и удалить 'self.data = "C"', тогда он печатает 'D'. Кроме того, если вы измените «struct ViewModelStruct» на «class ViewModelStruct», тогда он печатает «C». – san