Was ist der Zweck von willSet und didSet in Swift?

265

Swift hat eine Eigenschaftsdeklarationssyntax, die der von C # sehr ähnlich ist:

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

Es hat aber auch willSetund didSetAktionen. Diese werden vor bzw. nach dem Aufruf des Setters aufgerufen. Was ist ihr Zweck, wenn man bedenkt, dass Sie nur den gleichen Code im Setter haben könnten?

zneak
quelle
11
Ich persönlich mag hier nicht viele Antworten. Sie gehen zu weit in die Syntax hinein. Die Unterschiede betreffen eher die Semantik und die Lesbarkeit des Codes. Berechnete Eigenschaft ( get& set) besteht im Wesentlichen darin, eine Eigenschaft basierend auf einer anderen Eigenschaft berechnen zu lassen, z. B. die Umwandlung eines Etiketts textin ein Jahr Int. didSet& willSetgibt es zu sagen ... hey, dieser Wert wurde gesetzt, jetzt machen wir das, zB Unsere dataSource wurde aktualisiert ... also lasst uns die tableView neu laden, damit sie neue Zeilen enthält. Ein weiteres Beispiel finden Sie in der Antwort von dfri, wie Sie Delegierte anrufen könnendidSet
Honey

Antworten:

324

Der Punkt scheint zu sein, dass Sie manchmal eine Eigenschaft benötigen, die über automatischen Speicher und ein gewisses Verhalten verfügt, um beispielsweise andere Objekte darüber zu informieren, dass sich die Eigenschaft gerade geändert hat. Wenn Sie nur get/ haben set, benötigen Sie ein anderes Feld, um den Wert zu speichern. Mit willSetund didSetkönnen Sie Maßnahmen ergreifen, wenn der Wert geändert wird, ohne dass ein weiteres Feld erforderlich ist. Zum Beispiel in diesem Beispiel:

class Foo {
    var myProperty: Int = 0 {
        didSet {
            print("The value of myProperty changed from \(oldValue) to \(myProperty)")
        }
    }
}

myPropertyGibt bei jeder Änderung den alten und den neuen Wert aus. Mit nur Getter und Setter würde ich dies stattdessen brauchen:

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

So willSetund didSetstellt eine Wirtschaft von ein paar Zeilen, und weniger Lärm in der Feldliste.

zneak
quelle
248
Achtung: willSetund didSetwerden nicht aufgerufen, wenn Sie die Eigenschaft innerhalb einer Init-Methode festlegen, wie Apple feststellt: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
Dabei scheinen sie jedoch für eine Array-Eigenschaft aufgerufen zu werden: myArrayProperty.removeAtIndex(myIndex)... Nicht erwartet.
Andreas
4
Sie können die Zuweisung in eine defer {} -Anweisung innerhalb des Initialisierers einschließen, wodurch die Methoden willSet und didSet aufgerufen werden, wenn der Initialisiererbereich beendet wird. Ich empfehle es nicht unbedingt, sondern sage nur, dass es möglich ist. Eine der Konsequenzen ist, dass es nur funktioniert, wenn Sie die Eigenschaft als optional deklarieren, da sie nicht unbedingt vom Initialisierer initialisiert wird.
Marmoy
Bitte erläutern Sie die folgende Zeile. Ich verstehe nicht, ist diese Methode oder Variable var propertyChangedListener: (Int, Int) -> Void = {println ("Der Wert von myProperty hat sich von ($ 0) auf ($ 1) geändert")}
Vikash Rajput
Das Initialisieren von Eigenschaften in derselben Zeile wird in Swift 3 NICHT unterstützt. Sie sollten die Antwort so ändern, dass sie Swift 3 entspricht.
Ramazan Polat
149

Ich verstehe, dass set und get für berechnete Eigenschaften gelten (keine Sicherung von gespeicherten Eigenschaften ).

Wenn Sie von einem Objective-C kommen, denken Sie daran, dass sich die Namenskonventionen geändert haben. In Swift wird eine iVar- oder Instanzvariable als gespeicherte Eigenschaft bezeichnet

Beispiel 1 (schreibgeschützte Eigenschaft) - mit Warnung:

var test : Int {
    get {
        return test
    }
}

Dies führt zu einer Warnung, da dies zu einem rekursiven Funktionsaufruf führt (der Getter ruft sich selbst auf). Die Warnung in diesem Fall lautet "Versuch, 'Test' in seinem eigenen Getter zu ändern".

Beispiel 2. Bedingtes Lesen / Schreiben - mit Warnung

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

Ähnliches Problem - Sie können dies nicht tun, da der Setter rekursiv aufgerufen wird. Beachten Sie außerdem, dass sich dieser Code nicht über keine Initialisierer beschwert, da keine zu initialisierende Eigenschaft gespeichert ist .

Beispiel 3. Berechnete Lese- / Schreibeigenschaft - mit Hintergrundspeicher

Hier ist ein Muster, das die bedingte Einstellung einer tatsächlich gespeicherten Eigenschaft ermöglicht

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

Hinweis Die tatsächlichen Daten heißen _test (obwohl es sich um beliebige Daten oder Datenkombinationen handeln kann). Beachten Sie auch die Notwendigkeit, einen Anfangswert anzugeben (alternativ müssen Sie eine init-Methode verwenden), da _test tatsächlich eine Instanzvariable ist

Beispiel 4. Verwenden von will und did set

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

Hier sehen wir, wie willSet und didSet eine Änderung in einer tatsächlich gespeicherten Eigenschaft abfangen. Dies ist nützlich zum Senden von Benachrichtigungen, Synchronisieren usw. (siehe Beispiel unten).

Beispiel 5. Konkretes Beispiel - ViewController Container

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

Beachten Sie die Verwendung von BEIDEN berechneten und gespeicherten Eigenschaften. Ich habe eine berechnete Eigenschaft verwendet, um zu verhindern, dass derselbe Wert zweimal festgelegt wird (um zu verhindern, dass schlimme Dinge passieren!). Ich habe willSet und didSet verwendet, um Benachrichtigungen an viewController weiterzuleiten (siehe UIViewController-Dokumentation und Informationen zu viewController-Containern).

Ich hoffe das hilft und bitte jemanden schreien, wenn ich hier irgendwo einen Fehler gemacht habe!

user3675131
quelle
3
Warum kann ich didSet nicht zusammen mit get and set verwenden?
Ben Sinclair
//I can't see a way to 'stop' the value being set to the same controller - hence the computed property Warnung verschwinden, nachdem ich if let newViewController = _childVC { anstelle von if (_childVC) {
evfemist
5
get und set werden verwendet, um eine berechnete Eigenschaft zu erstellen. Dies sind reine Methoden, und es gibt keinen Sicherungsspeicher (Instanzvariable). willSet und didSet dienen zur Beobachtung von Änderungen an den Eigenschaften gespeicherter Variablen. Unter der Haube sind diese durch Aufbewahrung gesichert, aber in Swift ist alles in einem verschmolzen.
user3675131
In Ihrem Beispiel 5, in get, denke ich, müssen Sie hinzufügen if _childVC == nil { _childVC = something }und dann return _childVC.
JW.ZG
18

Diese werden als Immobilienbeobachter bezeichnet :

Immobilienbeobachter beobachten und reagieren auf Wertänderungen einer Immobilie. Eigenschaftsbeobachter werden jedes Mal aufgerufen, wenn der Wert einer Eigenschaft festgelegt wird, auch wenn der neue Wert mit dem aktuellen Wert der Eigenschaft übereinstimmt.

Auszug aus: Apple Inc. "Die schnelle Programmiersprache". iBooks. https://itun.es/ca/jEUH0.l

Ich vermute, es soll Dinge berücksichtigen, die wir traditionell mit KVO tun würden, wie Datenbindung mit UI-Elementen oder Auslösen von Nebenwirkungen beim Ändern einer Eigenschaft, Auslösen eines Synchronisierungsprozesses, Hintergrundverarbeitung usw. usw.

Sebastien Martin
quelle
16

HINWEIS

willSetund didSetBeobachter werden nicht aufgerufen, wenn eine Eigenschaft in einem Initialisierer festgelegt wird, bevor die Delegierung stattfindet

Bartłomiej Semańczyk
quelle
16

Sie können didSetdie Variable auch verwenden , um die Variable auf einen anderen Wert zu setzen. Dies führt nicht dazu, dass der Beobachter erneut angerufen wird, wie im Eigenschaftenhandbuch angegeben . Dies ist beispielsweise hilfreich, wenn Sie den Wert wie folgt begrenzen möchten:

let minValue = 1

var value = 1 {
    didSet {
        if value < minValue {
            value = minValue
        }
    }
}

value = -10 // value is minValue now.
knshn
quelle
10

Die vielen gut geschriebenen Antworten decken die Frage gut ab, aber ich werde im Detail eine Ergänzung erwähnen, die meiner Meinung nach eine Behandlung wert ist.


Die willSetund didSet-Eigenschaftsbeobachter können zum Aufrufen von Delegaten verwendet werden, z. B. für Klasseneigenschaften, die immer nur durch Benutzerinteraktion aktualisiert werden, bei denen Sie jedoch vermeiden möchten, dass der Delegat bei der Objektinitialisierung aufgerufen wird.

Ich zitiere Klaas hochgestimmten Kommentar zur akzeptierten Antwort:

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.

Dies ist recht ordentlich, da es bedeutet, dass die didSetEigenschaft beispielsweise eine gute Wahl für den Startpunkt für delegierte Rückrufe und Funktionen für Ihre eigenen benutzerdefinierten Klassen ist.

Betrachten Sie als Beispiel ein benutzerdefiniertes Benutzersteuerungsobjekt mit einer Schlüsseleigenschaft value(z. B. Position in der Bewertungssteuerung), die als Unterklasse von 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.
}

Danach können Ihre Delegatenfunktionen beispielsweise in einigen View-Controllern verwendet werden, um wichtige Änderungen im Modell zu beobachten CustomViewController, ähnlich wie Sie die inhärenten Delegatenfunktionen der UITextFieldDelegatefor- UITextFieldObjekte (z textFieldDidEndEditing(...). B. ) verwenden würden.

Verwenden Sie für dieses einfache Beispiel einen Delegatenrückruf von der didSetEigenschaft class value, um einem Ansichtscontroller mitzuteilen, dass einem seiner Outlets eine Modellaktualisierung zugeordnet ist:

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

Hier wurde die valueEigenschaft gekapselt, aber im Allgemeinen: Achten Sie in solchen Situationen darauf, die valueEigenschaft des customUserControlObjekts nicht im Bereich der zugehörigen Delegatenfunktion (hier :) didChangeValue()im Ansichts-Controller zu aktualisieren, da dies sonst der Fall ist unendliche Rekursion.

dfri
quelle
4

Die Beobachter willSet und didSet für die Eigenschaften, wenn der Eigenschaft ein neuer Wert zugewiesen wird. Dies gilt auch dann, wenn der neue Wert mit dem aktuellen Wert übereinstimmt.

Beachten Sie, dass willSetein Parametername didSeterforderlich ist , um dies zu umgehen.

Der didSet-Beobachter wird aufgerufen, nachdem der Wert der Eigenschaft aktualisiert wurde. Es vergleicht sich mit dem alten Wert. Wenn sich die Gesamtzahl der Schritte erhöht hat, wird eine Meldung gedruckt, die angibt, wie viele neue Schritte ausgeführt wurden. Der didSet-Beobachter gibt keinen benutzerdefinierten Parameternamen für den alten Wert an, und stattdessen wird der Standardname von oldValue verwendet.

Zigii Wong
quelle
2

Getter und Setter sind manchmal zu schwer zu implementieren, um die richtigen Wertänderungen zu beobachten. Normalerweise erfordert dies eine zusätzliche vorübergehende Variablenbehandlung und zusätzliche Überprüfungen, und Sie sollten selbst diese winzige Arbeit vermeiden, wenn Sie Hunderte von Gettern und Setzern schreiben. Diese Sachen sind für die Situation.

Eonil
quelle
1
Wollen Sie damit sagen, dass die Verwendung von und gegenüber gleichwertigem Setter-Code einen Leistungsvorteil bietet ? Dies scheint eine kühne Behauptung zu sein. willSetdidSet
Zneak
1
@zneak Ich habe ein falsches Wort verwendet. Ich beanspruche Programmieraufwand, nicht die Verarbeitungskosten.
Eonil
1

In Ihrer eigenen (Basis-) Klasse willSetund didSetsind ziemlich redundant , da Sie stattdessen eine berechnete Eigenschaft definieren können (dh get- und set-Methoden), die auf a zugreift und _propertyVariabledie gewünschte Vor- und Nachbearbeitung ausführt .

Wenn jedoch , Sie eine Klasse außer Kraft setzen , wenn die Eigenschaft bereits definiert , dann das willSetund didSetsind nützlich und nicht überflüssig!

Ragnarius
quelle
1

Eine Sache, die didSetwirklich praktisch ist, ist, wenn Sie Steckdosen verwenden, um zusätzliche Konfiguration hinzuzufügen.

@IBOutlet weak var loginOrSignupButton: UIButton! {
  didSet {
        let title = NSLocalizedString("signup_required_button")
        loginOrSignupButton.setTitle(title, for: .normal)
        loginOrSignupButton.setTitle(title, for: .highlighted)
  }
Orkoden
quelle
oder die Verwendung von willSet macht einige Auswirkungen auf diese Outlets-Methoden sinnvoll, nicht wahr?
Elia
-5

Ich kenne C # nicht, aber mit ein wenig Rätselraten denke ich, ich verstehe was

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

tut. Es sieht dem, was Sie in Swift haben, sehr ähnlich, aber es ist nicht dasselbe: In Swift haben Sie das getFoound nicht setFoo. Das ist kein kleiner Unterschied: Es bedeutet, dass Sie keinen zugrunde liegenden Speicher für Ihren Wert haben.

Swift hat Eigenschaften gespeichert und berechnet.

Eine berechnete Eigenschaft hat getund kann haben set(wenn es beschreibbar ist). Der Code im Getter und Setter muss dies jedoch in anderen Eigenschaften tun, wenn sie tatsächlich einige Daten speichern müssen . Es gibt keinen Hintergrundspeicher.

Eine gespeicherte Eigenschaft verfügt dagegen über einen Sicherungsspeicher. Aber es funktioniert nicht hat getund set. Stattdessen hat es willSetund didSetwas Sie verwenden können, um variable Änderungen zu beobachten und schließlich Nebenwirkungen auszulösen und / oder den gespeicherten Wert zu ändern. Sie haben willSetund didSetfür berechnete Eigenschaften nicht und Sie benötigen sie nicht, da Sie für berechnete Eigenschaften den Code verwenden können set, um Änderungen zu steuern.

Analoge Datei
quelle
Dies ist das Swift-Beispiel. getFoound setFoosind einfache Platzhalter für alles, was die Getter und Setter tun sollen. C # braucht sie auch nicht. (Ich habe ein paar syntaktische Feinheiten verpasst, als ich gefragt habe, bevor ich Zugriff auf den Compiler hatte.)
zneak
1
Oh ok. Der wichtige Punkt ist jedoch, dass eine berechnete Eigenschaft KEINEN zugrunde liegenden Speicher hat. Siehe auch meine andere Antwort: stackoverflow.com/a/24052566/574590
Analoge Datei