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

265

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

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

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

zneak
источник
11
Лично я не люблю много ответов здесь. Они слишком сильно опускаются в синтаксис. Различия скорее в семантике и читаемости кода. Вычисляемое свойство ( get& set) в основном должно иметь свойство, вычисляемое на основе другого свойства, например, путем преобразования метки textв год Int. didSet& willSetесть, что сказать ... эй, это значение было установлено, теперь давайте сделаем это, например, Наш источник данных был обновлен ... поэтому давайте перезагрузим tableView, чтобы он включал новые строки. В качестве другого примера см . Ответ dfri о том, как вызывать делегатовdidSet
Honey

Ответы:

324

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

zneak
источник
248
Внимание: willSetи didSetне вызываются, когда вы устанавливаете свойство из метода init, как отмечает Apple:willSet and didSet observers are not called when a property is first initialized. They are only called when the property’s value is set outside of an initialization context.
Klaas
4
Но они, кажется, вызываются для свойства массива при выполнении этого: myArrayProperty.removeAtIndex(myIndex)... Не ожидается.
Андреас
4
Вы можете заключить присваивание в оператор defer {} внутри инициализатора, который вызывает вызовы методов willSet и didSet при выходе из области инициализатора. Я не обязательно рекомендую это, просто говорю, что это возможно. Одним из последствий этого является то, что оно работает только в том случае, если вы объявляете свойство необязательным, поскольку оно не строго инициализируется из инициализатора.
Marmoy
Пожалуйста, объясните ниже. Я не понимаю, этот метод или переменная var propertyChangedListener: (Int, Int) -> Void = {println ("Значение myProperty изменилось с ($ 0) на ($ 1)")}
Викаш Раджпут,
Инициализация свойств в одной строке НЕ поддерживается в Swift 3. Вы должны изменить ответ, чтобы он соответствовал swift 3.
Ramazan Polat
149

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

если вы исходите из 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. Использование воли и набора

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

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

user3675131
источник
3
Почему нельзя использовать я использую didSet вместе с get и set ..?
Бен Синклер
//I can't see a way to 'stop' the value being set to the same controller - hence the computed property предупреждение исчезнет после того, как я использовал if let newViewController = _childVC { вместо if (_childVC) {
evfemist
5
get и set используются для создания вычисляемого свойства. Это чисто методы, и нет резервного хранилища (переменная экземпляра). willSet и didSet предназначены для наблюдения за изменениями свойств хранимых переменных. Под капотом они хранятся в хранилище, но в Swift все это объединено в одно целое.
user3675131
В вашем примере 5, getя думаю, вам нужно добавить, if _childVC == nil { _childVC = something }а затем return _childVC.
JW.ZG
18

Они называются Property Observers :

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

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

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

Себастьян Мартин
источник
16

НОТА

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

Бартломей Семанчик
источник
16

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

let minValue = 1

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

value = -10 // value is minValue now.
knshn
источник
10

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


В 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объектов for (например 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()в контроллере представления, иначе вы получите бесконечная рекурсия.

dfri
источник
4

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

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

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

Зигий Вонг
источник
2

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

Eonil
источник
1
Вы говорите , что есть производительность преимущество использования willSetи по didSetсравнению с эквивалентным кодом сеттера? Это похоже на смелое утверждение.
zneak
1
@zneak Я использовал не то слово. Я требую усилий программиста, а не затрат на обработку.
Eonil
1

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

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

ragnarius
источник
1

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

@IBOutlet weak var loginOrSignupButton: UIButton! {
  didSet {
        let title = NSLocalizedString("signup_required_button")
        loginOrSignupButton.setTitle(title, for: .normal)
        loginOrSignupButton.setTitle(title, for: .highlighted)
  }
orkoden
источник
или использование willSet имеет смысл для некоторых методов розеток, не так ли?
Элия
-5

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

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

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

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

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

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

Аналоговый файл
источник
Это пример Swift. getFooи setFooявляются простыми заполнителями для всего, что вы хотите, чтобы получатели и установщики делали. C # тоже не нуждается в них. (Я пропустил несколько синтаксических тонкостей, как я просил, прежде чем я получил доступ к компилятору.)
zneak
1
О хорошо Но важным моментом является то, что вычисляемое свойство НЕ имеет базового хранилища. Смотрите также мой другой ответ: stackoverflow.com/a/24052566/574590
Аналоговый файл