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

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

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

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

Ответ 1

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

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

Ответ 2

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

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

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

var test : Int {
    get {
        return 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
        }
    }
}

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

Пример 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 и установил

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

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

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

Ответ 3

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

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

Отрывок из: Apple Inc. "Быстрый язык программирования". интерактивные книги. https://itun.es/ca/jEUH0.l

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

Ответ 4

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

let minValue = 1

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

value = -10 // value is minValue now.

Ответ 5

Примечание

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

Ответ 6

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


Наблюдатели свойств 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()) в контроллере представления, или вы закончите бесконечную рекурсию.

Ответ 7

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

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

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

Ответ 8

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

Ответ 9

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

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

Ответ 10

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

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

Ответ 11

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

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

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

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

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

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