Ist es möglich, dass didSet während der Initialisierung in Swift aufgerufen wird?

217

Frage

In den Apple-Dokumenten wird Folgendes angegeben:

willSet- und didSet-Beobachter werden beim ersten Initialisieren einer Eigenschaft nicht aufgerufen. Sie werden nur aufgerufen, wenn der Wert der Eigenschaft außerhalb eines Initialisierungskontexts festgelegt wird.

Ist es möglich, diese während der Initialisierung aufzurufen?

Warum?

Nehmen wir an, ich habe diese Klasse

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

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

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

Ich habe die Methode erstellt doStuff, um die Verarbeitungsaufrufe präziser zu gestalten, aber ich möchte lieber nur die Eigenschaft innerhalb der didSetFunktion verarbeiten. Gibt es eine Möglichkeit, diesen Aufruf während der Initialisierung zu erzwingen?

Aktualisieren

Ich habe beschlossen, nur den Convenience-Intializer für meine Klasse zu entfernen und Sie zu zwingen, die Eigenschaft nach der Initialisierung festzulegen. Dadurch kann ich wissen, didSetdass immer aufgerufen wird. Ich habe nicht entschieden, ob dies insgesamt besser ist, aber es passt gut zu meiner Situation.

Logan
quelle
Es ist ehrlich gesagt am besten, einfach zu akzeptieren, dass Swift so funktioniert. Wenn Sie der var-Anweisung einen Inline-Initialisierungswert hinzufügen, ruft dies natürlich nicht "didSet" auf. Deshalb darf die Situation "init ()" auch nicht "didSet" aufrufen. es macht alles Sinn.
Fattie
@Logan Die Frage selbst hat meine Frage beantwortet;) danke!
AamirR
2
Wenn Sie den praktischen Initialisierer verwenden möchten, können Sie ihn kombinieren mit defer:convenience init(someProperty: AnyObject) { self.init() defer { self.someProperty = someProperty }
Darek Cieśla

Antworten:

100

Erstellen Sie eine eigene Set-Methode und verwenden Sie diese in Ihrer Init-Methode:

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

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

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

Indem Sie somePropertyals Typ deklarieren : AnyObject!(ein implizit entpacktes optionales Element), können Sie sich selbst vollständig initialisieren, ohne somePropertyfestgelegt zu werden. Wenn Sie anrufen, rufen setSomeProperty(someProperty)Sie ein Äquivalent von an self.setSomeProperty(someProperty). Normalerweise können Sie dies nicht tun, da self nicht vollständig initialisiert wurde. Da somePropertykeine Initialisierung erforderlich ist und Sie eine von self abhängige Methode aufrufen, verlässt Swift den Initialisierungskontext und didSet wird ausgeführt.

Oliver
quelle
3
Warum sollte sich dies von self.someProperty = newValue in der Init unterscheiden? Haben Sie einen funktionierenden Testfall?
mmmmmm
2
Ich weiß nicht warum es funktioniert - aber es funktioniert. Direktes Setzen des neuen Werts innerhalb von init () - Die Methode ruft didSet () nicht auf - aber mit der angegebenen Methode setSomeProperty ().
Oliver
50
Ich glaube ich weiß was hier passiert. Indem Sie somePropertyals Typ deklarieren AnyObject!(ein implizit entpacktes optionales Element), können Sie die selfvollständige Initialisierung durchführen, ohne somePropertyfestgelegt zu werden. Wenn Sie anrufen, rufen setSomeProperty(someProperty)Sie ein Äquivalent von an self.setSomeProperty(someProperty). Normalerweise können Sie dies nicht tun, da selfes nicht vollständig initialisiert wurde. Da somePropertykeine Initialisierung erforderlich ist und Sie eine von abhängige Methode aufrufen self, verlässt Swift den Initialisierungskontext und didSetwird ausgeführt.
Logan
3
Es sieht so aus, als würde alles, was außerhalb des eigentlichen init () gesetzt ist, didSet aufgerufen. Im wahrsten Sinne des Wortes hat alles, was nicht gesetzt init() { *HERE* }ist, didSet aufgerufen. Gut für diese Frage, aber die Verwendung einer sekundären Funktion zum Festlegen einiger Werte führt dazu, dass didSet aufgerufen wird. Nicht großartig, wenn Sie diese Setter-Funktion wiederverwenden möchten.
WCByrne
4
@Mark ernsthaft, der Versuch, Swift mit gesundem Menschenverstand oder Logik zu behandeln, ist einfach absurd. Aber Sie können den Upvotes vertrauen oder es selbst versuchen. Ich habe es gerade getan und es funktioniert. Und ja, es macht überhaupt keinen Sinn.
Dan Rosenstark
305

Wenn Sie deferinnerhalb eines initializer , für die Aktualisierung aller optionalen Eigenschaften oder ein weitere Aktualisierung nicht-optionale Eigenschaften , dass Sie bereits initialisiert haben und nach haben Sie genannt super.init()Methoden, dann willSet, didSetusw. genannt werden. Ich finde das bequemer als die Implementierung separater Methoden, mit denen Sie den Anruf an den richtigen Stellen verfolgen können.

Beispielsweise:

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

Wird ergeben:

I'm going to change to 3.14
Now I'm 3.14
Brian Westphal
quelle
6
Frecher kleiner Trick, ich mag es ... komische Eigenart von Swift.
Chris Hatton
4
Toll. Sie haben einen weiteren nützlichen Anwendungsfall gefunden defer. Vielen Dank.
Ryan
1
Gute Verwendung von Aufschub! Ich wünschte, ich könnte Ihnen mehr als eine Gegenstimme geben.
Christian Schnorr
3
sehr interessant .. Ich habe noch nie einen solchen Aufschub gesehen. Es ist beredt und funktioniert.
aBikis
Sie haben myOptionalField jedoch zweimal festgelegt, was aus Sicht der Leistung nicht besonders gut ist. Was ist, wenn schwere Berechnungen stattfinden?
Yakiv Kovalskyi
77

Als Variation von Olivers Antwort könnten Sie die Zeilen in einen Verschluss einwickeln. Z.B:

class Classy {

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

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

}

Edit: Brian Westphals Antwort ist imho schöner. Das Schöne an ihm ist, dass es auf die Absicht hinweist.

original_username
quelle
2
Das ist klug und verschmutzt die Klasse nicht mit redundanten Methoden.
Pointum
Tolle Antwort, danke! Beachten Sie, dass Sie in Swift 2.2 den Verschluss immer in Klammern setzen müssen.
Max
@ Max Cheers, das Beispiel enthält jetzt Parens
original_username
2
Kluge Antwort! Eine Anmerkung: Wenn Ihre Klasse eine Unterklasse von etwas anderem ist, müssen Sie super.init () vor ({self.foo = foo}) ()
kcstricks
1
@Hlung, ich habe nicht das Gefühl, dass es in Zukunft wahrscheinlicher ist als alles andere, aber es gibt immer noch das Problem, dass es ohne Kommentar des Codes nicht sehr klar ist, was es tut. Zumindest Brians "verzögerte" Antwort gibt dem Leser einen Hinweis, dass wir den Code nach der Initialisierung ausführen möchten.
original_username
10

Ich hatte das gleiche Problem und das funktioniert bei mir

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
quelle
wo hast du es her?
Wanja
3
Dieser Ansatz funktioniert nicht mehr. Der Compiler gibt zwei Fehler aus: - 'self', das im Methodenaufruf '$ defer' verwendet wird, bevor alle gespeicherten Eigenschaften initialisiert werden - Rückkehr vom Initialisierer ohne Initialisierung aller gespeicherten Eigenschaften
Edward B
Sie haben Recht, aber es gibt eine Problemumgehung. Sie müssen nur someProperty als implizit entpackt oder optional deklarieren und einen Standardwert mit Null-Koaleszenz angeben, um Fehler zu vermeiden. tldr; someProperty muss einen optionalen Typ sein
Mark
2

Dies funktioniert, wenn Sie dies in einer Unterklasse tun

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

Wird abeispielsweise didSetnicht ausgelöst. Wird aber zum bBeispiel didSetausgelöst, weil es in der Unterklasse ist. Es hat etwas mit dem zu tun, was initialization contextwirklich bedeutet, in diesem Fall hat sich das superclassdarum gekümmert

onmyway133
quelle
1

Dies ist zwar keine Lösung, aber eine alternative Vorgehensweise wäre die Verwendung eines Klassenkonstruktors:

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

    class func createInstance(someProperty: AnyObject) -> SomeClass {
        let instance = SomeClass() 
        instance.someProperty = someProperty
        return instance
    }  
}
Schneemann
quelle
1
Bei dieser Lösung müssen Sie die Option von ändern, somePropertyda sie bei der Initialisierung keinen Wert erhalten hat.
Mick MacCallum
1

In dem speziellen Fall, in dem Sie eine in Ihrer Oberklasse verfügbare Eigenschaft aufrufen willSetoder didSetin diese aufnehmen möchten init, können Sie Ihre Super-Eigenschaft einfach direkt zuweisen:

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

Beachten Sie, dass die Charlesism- Lösung mit einem Verschluss in diesem Fall immer auch funktionieren würde. Meine Lösung ist also nur eine Alternative.

Cœur
quelle
-1

Sie können es auf folgende Weise lösen:

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
quelle
1
Hallo @yshilov, ich glaube nicht, dass dies das Problem wirklich löst, auf das ich gehofft hatte, da Sie technisch gesehen den self.somePropertyInitialisierungsbereich verlassen , wenn Sie anrufen . Ich glaube, dass das Gleiche erreicht werden kann var someProperty: AnyObject! = nil { didSet { ... } }. Einige mögen es dennoch als Umgehungsmöglichkeit schätzen, danke für den Zusatz
Logan
@Logan nein, das wird nicht funktionieren. didSet wird in init () vollständig ignoriert.
Yshilov