Яка мета willSet та didSet у Swift?


265

У Swift є синтаксис декларації властивості, дуже схожий на C # 's:

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

Однак у нього є willSetі didSetдії. Вони називаються до і після виклику сетера відповідно. Яке їх призначення, враховуючи, що ви могли просто мати один і той же код всередині сетера?


11
Мені особисто тут не подобається багато відповідей. Вони занадто сильно спускаються в синтаксис. Відмінності більше стосуються семантики та читальності коду. В основному для обчислюваної власності ( get& set) властивість обчислюється на основі іншої властивості, наприклад, перетворення мітки textв рік Int. didSet& willSetє, щоб сказати ... ей це значення було встановлено, тепер давайте зробимо це, наприклад, наш ресурс даних був оновлений ... тому давайте перезавантажимо tableView, щоб воно включало нові рядки. Інший приклад дивіться у відповіді dfri про те, як викликати делегатівdidSet
Мед

Відповіді:


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представляють економію пари рядків і менше шуму в списку полів.


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 при закритті області ініціалізатора. Я не обов'язково рекомендую це, просто кажу, що це можливо. Одним із наслідків є те, що воно працює лише в тому випадку, якщо ви визнаєте властивість необов'язковою, оскільки вона не суворо ініціалізується з ініціалізатора.
Мармой

Будь ласка, поясніть нижче рядка. Я не отримую, це метод чи змінна var propertyChangedListener: (Int, Int) -> void = {println ("Значення моєї властивості змінилося з ($ 0) на ($ 1)")}
Vikash Rajput

Ініціалізація властивостей у цьому ж рядку НЕ підтримується в Swift 3. Ви повинні змінити відповідь, щоб відповідати швидкому 3.
Ramazan Polat

149

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

якщо ви приїжджаєте від 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
        }
    }
}

Аналогічна проблема - ви не можете цього зробити, оскільки це рекурсивно викликає сетер. Крім того, зверніть увагу, що цей код не поскаржиться на відсутність ініціалізаторів, оскільки немає збереженого властивості для ініціалізації .

Приклад 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
        }
    }
}

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

Я сподіваюся, що це допомагає, і, будь ласка, хтось кричить, якщо я десь тут помилився!


3
Чому не можу використовувати я використовую didSet разом із get and 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 and set використовуються для створення обчисленої властивості. Це суто методи, і резервного зберігання (змінної екземпляра) немає. willSet і didSet призначені для спостереження за змінами збережених змінних властивостей. Під кришкою вони підтримуються накопичувачем, але в Swift це все об'єднано в одне ціле.
user3675131

У вашому прикладі 5, в get, я думаю, вам потрібно додати if _childVC == nil { _childVC = something }і потім return _childVC.
JW.ZG

18

Вони називаються спостерігачами власності :

Спостерігачі власності спостерігають і реагують на зміни у цінності нерухомості. Спостерігачі властивостей викликаються щоразу, коли встановлюється значення властивості, навіть якщо нове значення збігається з поточним значенням властивості.

Уривок: Apple Inc. "Мова швидкого програмування". iBooks. 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.

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об'єктів (наприклад 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():) у контролері перегляду, інакше ви закінчите нескінченна рекурсія.


4

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

І зауважте, що willSetпотрібна назва параметра для обходу, з іншого боку, didSetне робить.

Спостерігач didSet викликається після оновлення значення властивості. Він порівнює зі старим значенням. Якщо загальна кількість кроків збільшилася, друкується повідомлення про те, скільки нових кроків було зроблено. Спостерігач didSet не надає спеціального імені параметра для старого значення, а замість нього використовується ім'я oldValue за замовчуванням.


2

Геттер і сетер іноді занадто важкі для реалізації, щоб просто спостерігати за правильними змінами значення. Зазвичай для цього потрібні додаткові тимчасові перемінні та додаткові перевірки, і ви хочете уникнути навіть цієї крихітної праці, якщо напишете сотні гетерів та сетерів. Ці напої для ситуації.


1
Ви говорите , що є продуктивність перевага використання willSetі по didSetпорівнянні з еквівалентним кодом сетера? Це здається сміливим позовом.
zneak

1
@zneak Я використав неправильне слово. Я претендую на зусилля програміста, а не на обробку.
Eonil

1

У вашому власному (базовому) класі willSetі didSetвони є надлишковими , оскільки ви можете замість цього визначити обчислену властивість (тобто методи get- і set), яка отримує доступ до _propertyVariableта виконує потрібні до- і післяпрограмування .

Якщо, проте , перевизначити клас , де властивість вже визначено , тоwillSet і didSetє корисними і не зайвими!


1

Одна річ , де didSetце дійсно зручно, коли ви використовуєте виходи для додавання додаткової настройки.

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

або використання willSet має сенс певних ефектів від цих методів, чи не так?
elia

-5

Я не знаю C #, але з невеликими здогадами я думаю, що розумію, що

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

робить. Це дуже схоже на те, що у вас у Swift, але це не те саме: у Swift у вас немає getFooі setFoo. Це не мала різниця: це означає, що ви не маєте базового сховища для вашої вартості.

Свіфт зберігає та обчислює властивості.

Обчислюване властивість має getі може мати set(якщо це можливо для запису). Але код у геттері та сеттері, якщо їм потрібно фактично зберігати одні дані, він повинен робити це в інших властивостях. Немає резервного зберігання.

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


Це приклад Swift. getFooі setFooє простими заповнювачами для того, що б ви хотіли робити мешканці та сетери. C # вони також не потрібні. (Я пропустив кілька синтаксичних тонкощів, як я запитав, перш ніж я отримав доступ до компілятора.)
zneak

1
О, добре. Але важливим моментом є те, що обчислюване властивість НЕ має базового сховища. Дивіться також мою іншу відповідь: stackoverflow.com/a/24052566/574590
Аналоговий файл
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.