Какова цель willSet и didSet в Swift?

Swift имеет синтаксис объявления свойства, очень похожий на C#:

var foo: Int {
    get { return getFoo() }
    set { setFoo(newValue) }
}

Тем не менее, он также имеет willSet а также didSet действия. Они вызываются до и после вызова установщика соответственно. Какова их цель, учитывая, что вы можете просто иметь один и тот же код внутри сеттера?

11 ответов

Решение

Суть в том, что иногда вам нужно свойство с автоматическим хранением и некоторым поведением, например, для уведомления других объектов о том, что это свойство только что изменилось. Когда все, что у вас есть, это get/setВам нужно другое поле для хранения значения. С willSet а также didSetВы можете действовать, когда значение изменяется без необходимости в другом поле. Например, в этом примере:

class Foo {
    var myProperty: Int = 0 {
        didSet {
            print("The value of myProperty changed from \(oldValue) to \(myProperty)")
        }
    }
}

myProperty печатает свое старое и новое значение каждый раз, когда оно модифицируется. Мне нужны только геттеры и сеттеры:

class Foo {
    var myPropertyValue: Int = 0
    var myProperty: Int {
        get { return myPropertyValue }
        set {
            print("The value of myProperty changed from \(myPropertyValue) to \(newValue)")
            myPropertyValue = newValue
        }
    }
}

Так willSet а также didSet представляют экономию в несколько строк и меньше шума в списке полей.

Насколько я понимаю, что set и get предназначены для вычисляемых свойств (без поддержки сохраненных свойств)

если вы исходите из Objective-C, имея в виду, что соглашения об именах изменились. В Swift iVar или переменная экземпляра называется сохраненным свойством

Пример 1 (свойство только для чтения) - с предупреждением:

var test : Int {
    get {
        return test
    }
}

Это приведет к предупреждению, потому что это приведет к рекурсивному вызову функции (геттер вызывает сам себя). Предупреждение в этом случае "Попытка изменить 'test' в своем собственном геттере".

Пример 2. Условное чтение / запись - с предупреждением

var test : Int {
    get {
        return test
    }
    set (aNewValue) {
        //I've contrived some condition on which this property can be set
        //(prevents same value being set)
        if (aNewValue != test) {
            test = aNewValue
        }
    }
}

Аналогичная проблема - вы не можете сделать это, так как он рекурсивно вызывает сеттер. Также обратите внимание, что этот код не будет жаловаться на отсутствие инициализаторов, так как нет хранимых свойств для инициализации.

Пример 3. чтение / запись вычисляемого свойства - с резервным хранилищем

Вот шаблон, который позволяет условную установку фактического сохраненного свойства

//True model data
var _test : Int = 0

var test : Int {
    get {
        return _test
    }
    set (aNewValue) {
        //I've contrived some condition on which this property can be set
        if (aNewValue != test) {
            _test = aNewValue
        }
    }
}

Примечание. Фактические данные называются _test (хотя это могут быть любые данные или комбинация данных). Обратите также внимание на необходимость предоставления начального значения (в качестве альтернативы вам необходимо использовать метод init), поскольку _test на самом деле является переменной экземпляра.

Пример 4. С помощью will и did set

//True model data
var _test : Int = 0 {

    //First this
    willSet {
        println("Old value is \(_test), new value is \(newValue)")
    }

    //value is set

    //Finaly this
    didSet {
        println("Old value is \(oldValue), new value is \(_test)")
    }
}

var test : Int {
    get {
        return _test
    }
    set (aNewValue) {
        //I've contrived some condition on which this property can be set
        if (aNewValue != test) {
            _test = aNewValue
        }
    }
}

Здесь мы видим, что willSet и didSet перехватывают изменение фактического сохраненного свойства. Это полезно для отправки уведомлений, синхронизации и т. Д. (См. Пример ниже)

Пример 5. Конкретный пример - контейнер ViewController

//Underlying instance variable (would ideally be private)
var _childVC : UIViewController? {
    willSet {
        //REMOVE OLD VC
        println("Property will set")
        if (_childVC != nil) {
            _childVC!.willMoveToParentViewController(nil)
            self.setOverrideTraitCollection(nil, forChildViewController: _childVC)
            _childVC!.view.removeFromSuperview()
            _childVC!.removeFromParentViewController()
        }
        if (newValue) {
            self.addChildViewController(newValue)
        }

    }

    //I can't see a way to 'stop' the value being set to the same controller - hence the computed property

    didSet {
        //ADD NEW VC
        println("Property did set")
        if (_childVC) {
//                var views  = NSDictionaryOfVariableBindings(self.view)    .. NOT YET SUPPORTED (NSDictionary bridging not yet available)

            //Add subviews + constraints
            _childVC!.view.setTranslatesAutoresizingMaskIntoConstraints(false)       //For now - until I add my own constraints
            self.view.addSubview(_childVC!.view)
            let views = ["view" : _childVC!.view] as NSMutableDictionary
            let layoutOpts = NSLayoutFormatOptions(0)
            let lc1 : AnyObject[] = NSLayoutConstraint.constraintsWithVisualFormat("|[view]|",  options: layoutOpts, metrics: NSDictionary(), views: views)
            let lc2 : AnyObject[] = NSLayoutConstraint.constraintsWithVisualFormat("V:|[view]|", options: layoutOpts, metrics: NSDictionary(), views: views)
            self.view.addConstraints(lc1)
            self.view.addConstraints(lc2)

            //Forward messages to child
            _childVC!.didMoveToParentViewController(self)
        }
    }
}


//Computed property - this is the property that must be used to prevent setting the same value twice
//unless there is another way of doing this?
var childVC : UIViewController? {
    get {
        return _childVC
    }
    set(suggestedVC) {
        if (suggestedVC != _childVC) {
            _childVC = suggestedVC
        }
    }
}

Обратите внимание на использование ОБА вычисленных и сохраненных свойств. Я использовал вычисляемое свойство, чтобы предотвратить установку одного и того же значения дважды (чтобы избежать неприятностей!); Я использовал willSet и didSet для пересылки уведомлений в viewController (см. Документацию UIViewController и информацию о контейнерах viewController)

Я надеюсь, что это поможет, и, пожалуйста, кто-то кричит, если я допустил ошибку где-нибудь здесь!

Они называются наблюдателями свойств:

Наблюдатели за недвижимостью наблюдают и реагируют на изменения стоимости имущества. Наблюдатели свойств вызываются каждый раз, когда устанавливается значение свойства, даже если новое значение совпадает с текущим значением свойства.

Выдержка из: Apple Inc. "Язык программирования Swift". IBooks. https://itun.es/ca/jEUH0.l

Я подозреваю, что это позволяет делать вещи, которые мы традиционно делаем с KVO, такие как привязка данных с элементами пользовательского интерфейса или побочные эффекты изменения свойства, запуска процесса синхронизации, фоновой обработки и т. Д. И т. Д. И т. Д.

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

let minValue = 1

var value = 1 {
    didSet {
        if value < minValue {
            value = minValue
        }
    }
}

value = -10 // value is minValue now.

НОТА

willSet а также didSet наблюдатели не вызываются, когда свойство устанавливается в инициализаторе до делегирования

Многие хорошо написанные существующие ответы хорошо охватывают этот вопрос, но я упомяну, в некоторых деталях, дополнение, которое, я считаю, заслуживает освещения.


willSet а также didSet Наблюдатели свойств могут использоваться для вызова делегатов, например, для свойств класса, которые когда-либо обновляются только при взаимодействии с пользователем, но где вы хотите избежать вызова делегата при инициализации объекта.

Я процитирую комментарий Клааса с одобренным ответом на принятый ответ:

Наблюдатели willSet и didSet не вызываются при первой инициализации свойства. Они вызываются только тогда, когда значение свойства установлено вне контекста инициализации.

Это довольно аккуратно, как это означает, например, didSet Свойство является хорошим выбором точки запуска для делегатов обратных вызовов и функций, для ваших собственных пользовательских классов.

В качестве примера рассмотрим некоторый пользовательский объект управления пользователя с некоторым ключевым свойством value (например, позиция в рейтинговом контроле), реализованная как подкласс UIView:

// CustomUserControl.swift
protocol CustomUserControlDelegate {
    func didChangeValue(value: Int)
    // func didChangeValue(newValue: Int, oldValue: Int)
    // func didChangeValue(customUserControl: CustomUserControl)
    // ... other more sophisticated delegate functions
}

class CustomUserControl: UIView {

    // Properties
    // ...
    private var value = 0 {
        didSet {
            // Possibly do something ...

            // Call delegate.
            delegate?.didChangeValue(value)
            // delegate?.didChangeValue(value, oldValue: oldValue)
            // delegate?.didChangeValue(self)
        }
    }

    var delegate: CustomUserControlDelegate?

    // Initialization
    required init?(...) { 
        // Initialise something ...

        // E.g. 'value = 1' would not call didSet at this point
    }

    // ... some methods/actions associated with your user control.
}

После чего ваши функции делегата могут быть использованы, скажем, в некотором контроллере представления, чтобы наблюдать ключевые изменения в модели для CustomViewControllerтак же, как вы бы использовали встроенные функции делегата UITextFieldDelegate за UITextField объекты (например, textFieldDidEndEditing(...)).

Для этого простого примера используйте обратный вызов делегата из didSet класса собственности value сообщить контроллеру представления, что с одним из его выходов было связано обновление модели:

// ViewController.swift
Import UIKit
// ...

class ViewController: UIViewController, CustomUserControlDelegate {

    // Properties
    // ...
    @IBOutlet weak var customUserControl: CustomUserControl!

    override func viewDidLoad() {
        super.viewDidLoad()
        // ...

        // Custom user control, handle through delegate callbacks.
        customUserControl = self
    }

    // ...

    // CustomUserControlDelegate
    func didChangeValue(value: Int) {
        // do some stuff with 'value' ...
    }

    // func didChangeValue(newValue: Int, oldValue: Int) {
        // do some stuff with new as well as old 'value' ...
        // custom transitions? :)
    //}

    //func didChangeValue(customUserControl: CustomUserControl) {
    //    // Do more advanced stuff ...
    //}
}

Здесь value свойство было инкапсулировано, но обычно: в подобных ситуациях будьте осторожны, чтобы не обновить value собственность customUserControl объект в области действия связанной функции делегата (здесь: didChangeValue()) в контроллере представления, или вы получите бесконечную рекурсию.

Наблюдатели willSet и didSet для свойств всякий раз, когда свойству присваивается новое значение. Это верно, даже если новое значение совпадает с текущим значением.

И обратите внимание, что willSet требуется имя параметра, чтобы обойти, с другой стороны, didSet не.

Обозреватель didSet вызывается после обновления значения свойства. Это сравнивается со старым значением. Если общее количество шагов увеличилось, печатается сообщение, указывающее, сколько новых шагов было сделано. Обозреватель didSet не предоставляет настраиваемое имя параметра для старого значения, и вместо него используется имя по умолчанию oldValue.

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

В вашем собственном (базовом) классе, willSet а также didSet довольно избыточны, так как вместо этого вы можете определить вычисляемое свойство (то есть методы get и set), которые обращаются к _propertyVariable и делает желаемое до и после обработки.

Однако если вы переопределите класс, в котором свойство уже определено, то willSet а также didSet полезны и не избыточны!

Одно где didSet действительно удобно, когда вы используете розетки для добавления дополнительной конфигурации.

@IBOutlet weak var loginOrSignupButton: UIButton! {
  didSet {
        let title = NSLocalizedString("signup_required_button")
        loginOrSignupButton.setTitle(title, for: .normal)
        loginOrSignupButton.setTitle(title, for: .highlighted)
  }

Я не знаю C#, но с небольшой догадкой, я думаю, что понимаю, что

foo : int {
    get { return getFoo(); }
    set { setFoo(newValue); }
}

делает. Это выглядит очень похоже на то, что у вас есть в Swift, но это не то же самое: в Swift у вас нет getFoo а также setFoo, Это не маленькая разница: это означает, что у вас нет базового хранилища для вашей ценности.

Swift имеет сохраненные и вычисленные свойства.

Вычисленное свойство имеет get и может иметь set (если это доступно для записи). Но код в методах получения и установки, если им действительно нужно хранить некоторые данные, должен делать это в других свойствах. Там нет резервного хранилища.

С другой стороны, сохраненное свойство имеет резервное хранилище. Но это не имеет get а также set, Вместо этого он имеет willSet а также didSet который вы можете использовать для наблюдения за изменениями переменных и, в конечном итоге, для запуска побочных эффектов и / или изменения сохраненного значения. У тебя нет willSet а также didSet для вычисляемых свойств, и они вам не нужны, потому что для вычисляемых свойств вы можете использовать код в set контролировать изменения.

Другие вопросы по тегам