Можно ли разрешить вызывать 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функции. Есть ли способ заставить это вызвать во время инициализации?

Обновить

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

логан
источник
честно говоря, лучше всего просто «принять, как это работает быстро». если вы помещаете встроенное значение инициализации в оператор var, конечно, это не вызывает "didSet". поэтому ситуация init () также не должна вызывать didSet. все это имеет смысл.
Толстяк
@Logan Сам вопрос ответил на мой вопрос;) спасибо!
AamirR
2
Если вы хотите использовать удобный инициализатор, вы можете объединить его с defer:convenience init(someProperty: AnyObject) { self.init() defer { self.someProperty = someProperty }
Darek Cieśla

Ответы:

100

Создайте собственный метод set и используйте его в своем методе init:

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

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

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

Объявляя somePropertyкак type: AnyObject!(неявно развернутый необязательный), вы позволяете self полностью инициализироваться без somePropertyустановки. Когда вы звоните, setSomeProperty(someProperty)вы звоните эквивалент self.setSomeProperty(someProperty). Обычно вы не сможете сделать это, потому что self не было полностью инициализировано. Поскольку somePropertyинициализация не требует инициализации, и вы вызываете метод, зависящий от себя, 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. Отлично подходит для этого вопроса, но использование вторичной функции для установки некоторых значений приводит к вызову didSet. Не очень хорошо, если вы хотите повторно использовать эту функцию установки.
WCByrne
4
@ Серьезно, пытаться применить здравый смысл или логику к Swift просто абсурдно. Но вы можете доверять ответам или попробовать сами. Я только что сделал, и это работает. И да, это не имеет никакого смысла вообще.
Дэн Розенстарк
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
Но вы устанавливаете myOptionalField дважды, что не очень хорошо с точки зрения производительности. Что делать, если проводятся тяжелые вычисления?
Яков Ковальский
77

Как вариант ответа Оливера, вы можете заключить строки в замыкание. Например:

class Classy {

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

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

}

Изменить: Брайан Вестфал ответ лучше imho. Приятно, что он намекает на намерения.

original_username
источник
2
Это умно и не загрязняет класс избыточными методами.
Pointum
Отличный ответ, спасибо! Стоит отметить, что в Swift 2.2 вам нужно всегда заключать замыкание в скобки.
Макс
@Max Cheers, пример включает в себя символы Parens сейчас
original_username
2
Умный ответ! Одно замечание: если ваш класс является подклассом чего-то другого, вам нужно вызвать super.init () перед ({self.foo = foo}) ()
kcstricks
1
@ Хлунг, у меня нет ощущения, что в будущем это скорее всего сломается, чем что-либо еще, но есть проблема, что без комментирования кода не очень понятно, что он делает. По крайней мере, ответ Брайана «отложить» дает читателю подсказку, что мы хотим выполнить код после инициализации.
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» перед инициализацией всех сохраненных свойств - возврат из инициализатора без инициализации всех сохраненных свойств
Эдвард Б
Вы правы, но есть обходной путь. Вам нужно только объявить someProperty как неявный развернутый или необязательный и предоставить ему значение по умолчанию с объединением nil, чтобы избежать сбоев. 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сделал заботу о том , что

onmyway133
источник
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-с способом:

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
    }
}
yshilov
источник
1
Привет @yshilov, я не думаю, что это действительно решает проблему, на которую я надеялся, так как технически, когда вы звоните, self.somePropertyвы выходите из области инициализации. Я верю, что то же самое может быть достигнуто, делая var someProperty: AnyObject! = nil { didSet { ... } }. Тем не менее, некоторые могут оценить это как
Logan
@Logan Нет, это не сработает. didSet будет полностью игнорироваться в init ().
yshilov