Чи можна дозволити виклик didSet під час ініціалізації у Swift?


217

Питання

Документи Apple вказують, що:

willSet та didSet спостерігачі не викликаються, коли властивість вперше ініціалізується. Вони викликаються лише тоді, коли значення властивості встановлено поза контекстом ініціалізації.

Чи можна змусити їх викликати під час ініціалізації?

Чому?

Скажімо, у мене цей клас

class SomeClass {
    var someProperty: AnyObject {
        didSet {
            doStuff()
        }
    }

    init(someProperty: AnyObject) {
        self.someProperty = someProperty
        doStuff()
    }

    func doStuff() {
        // do stuff now that someProperty is set
    }
}

Я створив метод doStuff, щоб зробити виклики обробки більш стислими, але я краще просто обробити властивість у межах didSetфункції. Чи є спосіб змусити це викликати під час ініціалізації?

Оновлення

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


чесно найкраще просто "прийняти це, як швидко працює". якщо ви ставите вбудоване значення ініціалізації на оператор var, звичайно, що не викликає "didSet". тому ситуація "init ()" також не повинна називати "didSet". це все має сенс.
Fattie

@Logan Саме запитання відповіло на моє запитання;) дякую!
AamirR

2
Якщо ви хочете використовувати ініціалізатор зручності, ви можете комбінувати його з defer:convenience init(someProperty: AnyObject) { self.init() defer { self.someProperty = someProperty }
Darek Cieśla

Відповіді:


100

Створіть власний набір-метод та використовуйте його у своєму методі init:

class SomeClass {
    var someProperty: AnyObject! {
        didSet {
            //do some Stuff
        }
    }

    init(someProperty: AnyObject) {
        setSomeProperty(someProperty)
    }

    func setSomeProperty(newValue:AnyObject) {
        self.someProperty = newValue
    }
}

Оголошуючи somePropertyтип: AnyObject!(неявно розгорнута опція), ви дозволяєте self повністю ініціалізуватися без somePropertyвстановлення. Коли ви телефонуєте, ви дзвоните setSomeProperty(someProperty)в еквівалент self.setSomeProperty(someProperty). Зазвичай ви цього не зможете зробити, тому що самостійно не було повністю ініціалізовано. Оскільки somePropertyне потрібна ініціалізація, і ви викликаєте метод, залежний від self, Swift залишає контекст ініціалізації і didSet запускається.


3
Чому це відрізняється від self.someProperty = newValue в init? У вас є робочий тестовий випадок?
мммммм

2
Не знаю, чому це працює - але це так. Безпосереднє встановлення нового значення в init () - метод не викликає didSet () - але використовує даний метод setSomeProperty ().
Олівер

50
Я думаю, я знаю, що тут відбувається. Заявивши somePropertyяк тип: AnyObject!(неявно розгорнута опція), ви дозволяєте selfповністю ініціалізуватись без somePropertyвстановлення. Коли ви телефонуєте, ви дзвоните setSomeProperty(someProperty)в еквівалент self.setSomeProperty(someProperty). Зазвичай ви цього не зможете зробити, оскільки selfне були повністю ініціалізовані. Оскільки somePropertyне потрібна ініціалізація, і ви викликаєте метод, залежний від цього self, Swift залишає контекст ініціалізації та didSetзапускається.
Логан

3
Схоже, що все, що встановлено поза фактичного init () НЕ отримує didSet. Буквально все, що не встановлено init() { *HERE* }, зробив би виклик. Чудово підходить для цього питання, але використання вторинної функції для встановлення деяких значень призводить до виклику didSet. Не чудово, якщо ви хочете повторно використовувати цю функцію налаштування.
WCByrne

4
@ Марк серйозно, намагатися застосувати здоровий глузд або логіку до Свіфта - просто абсурд. Але ви можете довіряти оновленим або спробувати самостійно. Я щойно зробив, і це працює. І так, це взагалі не має сенсу.
Дан Розенстарк

305

Якщо ви використовуєте deferвсередині з ініціалізатор , для оновлення всіх додаткових властивостей або подальшого оновлення без додаткових властивостей , які ви вже ініційовані і після того, як ви назвали які - або super.init()методи, то ваш willSet, didSetі т.д. буде називатися. Я вважаю це більш зручним, ніж впровадження окремих методів, які вам слід буде відслідковувати дзвінки в потрібних місцях.

Наприклад:

public class MyNewType: NSObject {

    public var myRequiredField:Int

    public var myOptionalField:Float? {
        willSet {
            if let newValue = newValue {
                print("I'm going to change to \(newValue)")
            }
        }
        didSet {
            if let myOptionalField = self.myOptionalField {
                print("Now I'm \(myOptionalField)")
            }
        }
    }

    override public init() {
        self.myRequiredField = 1

        super.init()

        // Non-defered
        self.myOptionalField = 6.28

        // Defered
        defer {
            self.myOptionalField = 3.14
        }
    }
}

Вийде:

I'm going to change to 3.14
Now I'm 3.14

6
Хитрий маленький трюк, мені це подобається ... дивна химерність Свіфта.
Кріс Хаттон

4
Чудово. Ви знайшли ще один корисний випадок використання defer. Дякую.
Райан

1
Прекрасне використання відкладення! Хотілося б, щоб я міг дати вам більше одного внеску.
Крістіан Шнорр

3
дуже цікаво .. Я ніколи не бачив відкладених раніше таких. Це красномовно і працює.
aBikis

Але ви встановлюєте myOtionalField двічі, що не є великим з точки зору продуктивності. Що робити, якщо відбудуться важкі розрахунки?
Яків Ковальський

77

Як варіант відповіді Олівера, ви можете загорнути лінії в замикання. Наприклад:

class Classy {

    var foo: Int! { didSet { doStuff() } }

    init( foo: Int ) {
        // closure invokes didSet
        ({ self.foo = foo })()
    }

}

Редагувати: Відповідь Брайана Вестфала - це приємніше. Приємне в його тому, що це натякає на наміри.


2
Це розумно і не забруднює клас зайвими методами.
pointum

Чудова відповідь, дякую! Варто зауважити, що у Swift 2.2 потрібно закривати закриття в дужках завжди.
Макс

@Max Cheers, зразок включає паролі зараз
original_username

2
Розумна відповідь! Одна примітка: якщо ваш клас є підкласом чогось іншого, вам потрібно буде зателефонувати до super.init () раніше ({self.foo = foo}) ()
kcstricks

1
@Hlung, я не маю відчуття, що це швидше зламається в майбутньому, ніж будь-що інше, але все ще існує проблема, що без коментування коду не дуже зрозуміло, що він робить. Принаймні відповідь Брайана на "відстрочку" дає читачеві підказку, що ми хочемо виконати код після ініціалізації.
original_username

10

У мене була така ж проблема, і це працює для мене

class SomeClass {
    var someProperty: AnyObject {
        didSet {
            doStuff()
        }
    }

    init(someProperty: AnyObject) {
        defer { self.someProperty = someProperty }
    }

    func doStuff() {
        // do stuff now that someProperty is set
    }
}

де ти це взяв?
Ваня

3
Цей підхід вже не працює. Компілятор видає дві помилки: - "self", який використовується у виклику методу "$ defer", перш ніж всі збережені властивості ініціалізуються, - Повернення з ініціалізатора без ініціалізації всіх збережених властивостей
Edward B

Ви маєте рацію, але є рішення. Вам потрібно лише оголосити деяку власність неявно розгорнутою або необов'язковою і поставити їй значення за замовчуванням з нульовим згортанням, щоб уникнути збоїв. tldr; someProperty має бути необов’язковим типом
Марк

2

Це працює, якщо ви робите це в підкласі

class Base {

  var someProperty: AnyObject {
    didSet {
      doStuff()
    }
  }

  required init() {
    someProperty = "hello"
  }

  func doStuff() {
    print(someProperty)
  }
}

class SomeClass: Base {

  required init() {
    super.init()

    someProperty = "hello"
  }
}

let a = Base()
let b = SomeClass()

У aприкладі, didSetне спрацьовує. Але, bнаприклад, didSetспрацьовує, тому що знаходиться в підкласі. Він повинен щось робити з тим, що initialization contextнасправді означає, в даному випадку superclassсправді це піклується


1

Хоча це не рішення, альтернативним способом вирішення цього питання буде використання конструктора класів:

class SomeClass {
    var someProperty: AnyObject {
        didSet {
            // do stuff
        }
    }

    class func createInstance(someProperty: AnyObject) -> SomeClass {
        let instance = SomeClass() 
        instance.someProperty = someProperty
        return instance
    }  
}

1
Це рішення вимагатиме від вас змінити необов'язковість, somePropertyоскільки воно не дасть йому значення під час ініціалізації.
Мік Маккаллум

1

У конкретному випадку, коли ви хочете покликатись willSetабо didSetвсередині initвластивості, наявної у вашому суперкласі, ви можете просто призначити свою супермаєтність безпосередньо:

override init(frame: CGRect) {
    super.init(frame: frame)
    // this will call `willSet` and `didSet`
    someProperty = super.someProperty
}

Зауважте, що рішення Карла з закриттям завжди працювало б і в цьому випадку. Тож моє рішення - це просто альтернатива.


-1

Ви можете вирішити це способом obj-s:

class SomeClass {
    private var _someProperty: AnyObject!
    var someProperty: AnyObject{
        get{
            return _someProperty
        }
        set{
            _someProperty = newValue
            doStuff()
        }
    }
    init(someProperty: AnyObject) {
        self.someProperty = someProperty
        doStuff()
    }

    func doStuff() {
        // do stuff now that someProperty is set
    }
}

1
Привіт @yshilov, я не думаю, що це насправді вирішує проблему, на яку я сподівався технічно, коли ви телефонуєте, self.somePropertyви залишаєте область ініціалізації. Я вважаю, що те ж саме можна було зробити, роблячи var someProperty: AnyObject! = nil { didSet { ... } }. І все-таки дехто може оцінити це як подорож, спасибі за додаток
Логан

@ Логан ні, це не буде працювати. inSit буде повністю ігноровано в init ().
yshilov
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.