Czy można zezwolić na wywoływanie didSet podczas inicjalizacji w Swift?

217

Pytanie

Dokumenty Apple'a określają, że:

Obserwatory willSet i didSet nie są wywoływane przy pierwszej inicjalizacji właściwości. Są wywoływane tylko wtedy, gdy wartość właściwości jest ustawiona poza kontekstem inicjalizacji.

Czy można wymusić ich wywoływanie podczas inicjalizacji?

Czemu?

Powiedzmy, że mam tę klasę

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

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

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

Stworzyłem metodę doStuff, aby wywołania przetwarzania były bardziej zwięzłe, ale wolałbym po prostu przetworzyć właściwość w ramach didSetfunkcji. Czy istnieje sposób, aby zmusić to do wywołania podczas inicjalizacji?

Aktualizacja

Postanowiłem po prostu usunąć wygodny interializer dla mojej klasy i zmusić cię do ustawienia właściwości po inicjalizacji. To pozwala mi wiedzieć, didSetże zawsze będzie dzwonić. Nie zdecydowałem, czy jest to ogólnie lepsze, ale dobrze pasuje do mojej sytuacji.

Logan
źródło
najlepiej jest po prostu „zaakceptować to, jak działa szybkie”. jeśli wstawisz wartość inicjującą inline do instrukcji var, to oczywiście nie wywoła „didSet”. dlatego sytuacja „init ()” nie może również wywoływać „didSet”. to wszystko ma sens.
Fattie
@ Logan Samo pytanie odpowiedziało na moje pytanie;) dzięki!
AamirR
2
Jeśli chcesz skorzystać z inicjalizatora wygody, możesz połączyć go z defer:convenience init(someProperty: AnyObject) { self.init() defer { self.someProperty = someProperty }
Darek Cieśla

Odpowiedzi:

100

Utwórz własną metodę zestawu i użyj jej w metodzie init:

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

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

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

Deklarując somePropertyjako typ: AnyObject!(domyślnie nieopakowane opcjonalne), pozwalasz sobie na pełną inicjalizację bez somePropertyustawiania. Kiedy dzwonisz setSomeProperty(someProperty), dzwonisz do równowartości self.setSomeProperty(someProperty). Zwykle nie byłbyś w stanie tego zrobić, ponieważ jaźń nie została w pełni zainicjowana. Ponieważ somePropertynie wymaga inicjalizacji, a ty wywołujesz metodę zależną od siebie, Swift opuszcza kontekst inicjalizacji i didSet uruchomi się.

Oliver
źródło
3
Dlaczego miałoby się to różnić od self.someProperty = newValue w init? Czy masz sprawny przypadek testowy?
mmmmmm,
2
Nie wiem, dlaczego to działa - ale działa. Bezpośrednie ustawienie nowej wartości w ramach init () - Metoda nie wywołuje didSet () - ale używa podanej metody setSomeProperty ().
Oliver,
50
Chyba wiem, co się tutaj dzieje. Deklarując somePropertyjako typ: AnyObject!(domyślnie nieopakowane opcjonalne), pozwalasz selfna pełną inicjalizację bez somePropertyustawiania. Kiedy dzwonisz setSomeProperty(someProperty), dzwonisz do równowartości self.setSomeProperty(someProperty). Zwykle nie byłbyś w stanie tego zrobić, ponieważ selfnie został w pełni zainicjowany. Ponieważ somePropertynie wymaga inicjalizacji i wywołujesz metodę zależną od self, Swift opuszcza kontekst inicjalizacji i didSeturuchamia się.
Logan,
3
Wygląda na to, że wszystko, co jest ustawione poza faktyczną init (), wywołuje didSet. Dosłownie wszystko, co nie jest ustawione, init() { *HERE* }będzie miało wywołanie didSet. Idealne do tego pytania, ale użycie dodatkowej funkcji do ustawienia niektórych wartości prowadzi do wywołania didSet. Nie wspaniale, jeśli chcesz ponownie użyć tej funkcji.
WCByrne
4
@ Mark poważnie, próba zastosowania zdrowego rozsądku lub logiki w Swift jest po prostu absurdalna. Ale możesz zaufać głosom lub spróbować samemu. Właśnie to zrobiłem i działa. I tak, to nie ma żadnego sensu.
Dan Rosenstark,
305

Jeśli używasz deferwewnątrz z inicjatora , w celu aktualizacji opcjonalnych właściwości lub dalszej aktualizacji non-opcjonalne właściwości, które zostały już zainicjowane i po tym, jak nazywa się żadnych super.init()metod, wówczas willSet, didSetitp zostanie wywołana. Uważam, że jest to wygodniejsze niż wdrażanie osobnych metod, które musisz śledzić w odpowiednich miejscach.

Na przykład:

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

Przyniesie:

I'm going to change to 3.14
Now I'm 3.14
Brian Westphal
źródło
6
Bezczelna sztuczka, podoba mi się ... dziwne dziwactwo Szybkiego.
Chris Hatton,
4
Wspaniały. Znalazłeś inny użyteczny przypadek użycia defer. Dzięki.
Ryan
1
Świetne wykorzystanie odroczenia! Chciałbym dać ci więcej niż jedną opinię.
Christian Schnorr
3
bardzo interesujące .. Nigdy wcześniej nie widziałem takiego odroczenia. Jest wymowny i działa.
aBikis
Ale ustawiłeś myOptionalField dwa razy, co nie jest świetne z punktu widzenia wydajności. Co się stanie, jeśli zostaną przeprowadzone ciężkie obliczenia?
Yakiv Kovalskyi
77

Jako wariant odpowiedzi Olivera możesz zawinąć linie w zamknięcie. Na przykład:

class Classy {

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

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

}

Edycja: Odpowiedź Briana Westphala jest ładniejsza imho. Zaletą jego jest to, że wskazuje to na intencję.

oryginalna nazwa użytkownika
źródło
2
To sprytne i nie zanieczyszcza klasy zbędnymi metodami.
punkt
Świetna odpowiedź, dzięki! Warto zauważyć, że w Swift 2.2 zawsze należy zawijać zamknięcie w nawiasach.
Maks.
@ Max Cheers, próbka zawiera teraz parens
original_username
2
Sprytna odpowiedź! Jedna uwaga: jeśli twoja klasa jest podklasą czegoś innego, musisz wywołać super.init () przed ({self.foo = foo}) ()
kcstricks
1
@Hlung, nie mam wrażenia, że ​​bardziej prawdopodobne jest, że złamie się w przyszłości niż cokolwiek innego, ale nadal istnieje problem polegający na tym, że bez komentowania kodu nie jest jasne, co robi. Przynajmniej „odroczona” odpowiedź Briana daje czytelnikowi wskazówkę, że chcemy wykonać kod po inicjalizacji.
original_username
10

Miałem ten sam problem i to działa dla mnie

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

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

    func doStuff() {
        // do stuff now that someProperty is set
    }
}
Carmine Cuofano
źródło
Skąd to masz?
Vanya
3
To podejście już nie działa. Kompilator zgłasza dwa błędy: - „self” użyte w wywołaniu metody „$ defer” przed zainicjowaniem wszystkich przechowywanych właściwości - Powrót z inicjalizatora bez inicjowania wszystkich przechowywanych właściwości
Edward B
Masz rację, ale istnieje obejście. Musisz tylko zadeklarować niektóre właściwości jako niejawne, nieopakowane lub opcjonalne i podać wartość domyślną z zerowym łączeniem, aby uniknąć awarii. tldr; someProperty musi być typem opcjonalnym
Mark
2

Działa to, jeśli zrobisz to w podklasie

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

Na aprzykład didSetnie jest wyzwalany. Ale na bprzykład didSetjest wyzwalany, ponieważ znajduje się w podklasie. To ma coś wspólnego z tym, co initialization contextnaprawdę oznacza, w tym przypadku o superclassto dbało

onmyway133
źródło
1

Chociaż nie jest to rozwiązanie, alternatywnym sposobem obejścia tego problemu byłoby użycie konstruktora klasy:

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

    class func createInstance(someProperty: AnyObject) -> SomeClass {
        let instance = SomeClass() 
        instance.someProperty = someProperty
        return instance
    }  
}
Bałwan
źródło
1
To rozwiązanie wymagałoby zmiany opcjonalności, somePropertyponieważ nie dałoby jej wartości podczas inicjalizacji.
Mick MacCallum
1

W szczególnym przypadku, w którym chcesz wywołać właściwość dostępną w nadklasie willSetlub didSetwewnątrz initniej, możesz po prostu przypisać swoją super właściwość bezpośrednio:

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

Zauważ, że rozwiązanie Charlesism z zamknięciem zawsze działałoby również w tym przypadku. Więc moje rozwiązanie jest tylko alternatywą.

Cœur
źródło
-1

Możesz to rozwiązać w obj-с sposób:

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
    }
}
jesziłow
źródło
1
Cześć @yshilov, Nie sądzę, że to naprawdę rozwiązuje problem, na który miałem nadzieję, ponieważ technicznie, kiedy dzwonisz self.someProperty, opuszczasz zakres inicjalizacji. Wierzę, że to samo można osiągnąć, robiąc var someProperty: AnyObject! = nil { didSet { ... } }. Mimo to niektórzy mogą docenić to jako obejście, dzięki za dodanie
Logan
@ Logan nie, to nie zadziała. didSet zostanie całkowicie zignorowany w init ().
yshilov