2016-06-16 6 views
11

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

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

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

Но на шаге 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. Может ли кто-нибудь помочь мне понять это?

+0

Вы хотите сказать, что он не меняет viewModel.data после вызова метода changeFromClass? – iamyogish

+0

Он меняет его после changeFromClass (и до changeFromStruct), но это изменение просто не видно внутри changeFromClass. –

+0

Это действительно для меня. Я считаю, что Swift должен попытаться сделать некоторую оптимизацию согласно следующей заметке Apple. 'В качестве оптимизации Swift может вместо этого захватить и сохранить копию значения, если это значение не будет мутировано закрытием, и если значение не будет мутировано после создания замыкания. '..... Если вы добавите эту строку 'self.data =" D "' после 'let networkingClass = NetworkingClass()' и удалить 'self.data = "C"', тогда он печатает 'D'. Кроме того, если вы измените «struct ViewModelStruct» на «class ViewModelStruct», тогда он печатает «C». – san

ответ

0

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

enter image description here

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

Точно моя точка. Мы можем проверить это, распечатав self.viewModel.data после viewModel.changeFromClass (до viewModel.changeFromStruct), он печатает «C». Поэтому вызовите viewModel.changeFromClass, фактически меняя viewModel, но он не виден внутри закрытия. Я предполагаю, что внутри viewModel.changeFromClass закрытие self.viewModel фиксируется в текущем состоянии, то есть ViewModelStruct (данные: «A»), и это то, что мы печатаем.мутирующая функция ViewModel меняет структуру, которая видна viewController, но не видна закрытию. –

+0

Вы должны учитывать, что функция fetchDataOverNetwork будет несинхронизировать. Я отредактировал его async, модель просмотра в классе не изменилась.'1 ViewModelStruct (данные: «A») 2 ViewModelStruct (данные: «B») 3 ViewModelStruct (данные: «A») A 4 ViewModelStruct (данные : "B") ' – Paul

+0

@tarun_sharma Вы решили его? Я думаю, что функция mutating изменила значение сразу после возвращения функции. Таким образом, при использовании async func (например, выборка данных по сети) значение в представлении контроллера изменилось сразу после функции mutating (назначено новое значение). По завершении работы сети она изменила данные в исходном значении, а не на новое значение. Я столкнулся с такой же ситуацией и еще не нашел решения. Если вы нашли решение, разместите его? Это делает меня сумасшедшим. :( – Paul

2

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

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

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

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

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

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

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

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

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

    % 3 = alloc_box $ ViewModelStruct, вар, имя "Я", 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 предполагается вытекающим типа, во время выполнения он на самом деле оказывается в 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 сделает запрос на веб-службу и вернуться. Когда приходит ответ, завершение будет вызвано. Таким образом, он всегда будет иметь экранирующий тип. Это создаст ту же проблему. Некоторыми уродливыми решениями для этого могут быть:

  1. Сделать ViewModel классом not struct. Это гарантирует, что 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. 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:

  1. Поместите весь код в стремительном файле.

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

    swiftc -emit-силь StructsInClosure.swift> output.txt

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

+0

Я столкнулся с такой же проблемой при модели MVVM. Спасибо за детальную объяснение Тион. –

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 работает хорошо! Но для этого нужно удалить принудительное литье в методе делегата.