Qual è lo scopo di willSet e didSet in Swift?


265

Swift ha una sintassi della dichiarazione di proprietà molto simile a quella di C #:

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

Tuttavia, ha anche willSete didSetazioni. Questi vengono chiamati prima e dopo il setter, rispettivamente. Qual è il loro scopo, considerando che potresti avere lo stesso codice all'interno del setter?


11
Personalmente non mi piacciono molte risposte qui. Scendono troppo nella sintassi. Le differenze riguardano maggiormente la semantica e la leggibilità del codice. Le proprietà calcolate ( get& set) devono fondamentalmente avere una proprietà calcolata sulla base di un'altra proprietà, ad esempio convertendo un'etichetta textin un anno Int. didSete willSetsiamo qui per dire ... ehi, questo valore è stato impostato, ora facciamolo, ad es. Il nostro dataSource è stato aggiornato ... quindi ricarichiamo tableView in modo che includa nuove righe. Per un altro esempio, vedi la risposta di dfri su come chiamare i delegatididSet
Honey,

Risposte:


324

Il punto sembra essere che a volte sia necessaria una proprietà con memoria automatica e alcuni comportamenti, ad esempio per notificare ad altri oggetti che la proprietà è appena cambiata. Quando tutto ciò che hai è get/ set, hai bisogno di un altro campo per contenere il valore. Con willSete didSet, puoi agire quando il valore viene modificato senza che sia necessario un altro campo. Ad esempio, in quell'esempio:

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

myPropertystampa il suo valore vecchio e nuovo ogni volta che viene modificato. Con solo getter e setter, avrei bisogno di questo invece:

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

Quindi willSete didSetrappresentano un'economia di un paio di linee e meno rumore nell'elenco dei campi.


248
Attenzione: willSete didSetnon vengono chiamati quando si imposta la proprietà all'interno di un metodo init come nota Apple: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
Ma sembrano essere chiamati su una proprietà array quando fanno questo: myArrayProperty.removeAtIndex(myIndex)... Non previsto.
Andreas,

4
È possibile racchiudere l'assegnazione in un'istruzione defer {} all'interno dell'inizializzatore che provoca la chiamata ai metodi willSet e didSet quando viene chiuso l'ambito dell'inizializzatore. Non lo raccomando necessariamente, sto solo dicendo che è possibile. Una delle conseguenze è che funziona solo se si dichiara la proprietà facoltativa, poiché non è strettamente inizializzata dall'inizializzatore.
Marmoy,

Si prega di spiegare sotto la riga. Non sto ottenendo, questo metodo o la variabile var propertyChangedListener: (Int, Int) -> Void = {println ("Il valore di myProperty è cambiato da ($ 0) a ($ 1)")}
Vikash Rajput

L'inizializzazione delle proprietà nella stessa riga NON è supportata in Swift 3. Dovresti cambiare la risposta in conformarsi a swift 3.
Ramazan Polat

149

La mia comprensione è che set e get sono per le proprietà calcolate (nessun supporto dalle proprietà memorizzate )

se si proviene da un Objective-C, tenendo presente che le convenzioni di denominazione sono cambiate. In Swift una variabile iVar o di istanza è denominata proprietà memorizzata

Esempio 1 (proprietà di sola lettura) - con avviso:

var test : Int {
    get {
        return test
    }
}

Ciò si tradurrà in un avviso perché si traduce in una chiamata di funzione ricorsiva (il getter chiama se stesso). L'avviso in questo caso è "Tentativo di modificare 'test' all'interno del proprio getter".

Esempio 2. Lettura / scrittura condizionale - con avviso

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

Problema simile: non puoi farlo poiché chiama ricorsivamente il setter. Inoltre, si noti che questo codice non si lamenterà di nessun inizializzatore poiché non è presente alcuna proprietà memorizzata da inizializzare .

Esempio 3. proprietà calcolata lettura / scrittura - con archivio di backup

Ecco un modello che consente l'impostazione condizionale di una proprietà memorizzata effettiva

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

Nota I dati effettivi sono chiamati _test (anche se potrebbero essere dati o combinazioni di dati) Nota anche la necessità di fornire un valore iniziale (in alternativa è necessario utilizzare un metodo init) poiché _test è in realtà una variabile di istanza

Esempio 4. Utilizzo di will e 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
        }
    }
}

Qui vediamo willSet e didSet intercettare una modifica in una proprietà memorizzata effettiva. Questo è utile per l'invio di notifiche, sincronizzazione ecc ... (vedi esempio sotto)

Esempio 5. Esempio concreto: contenitore ViewController

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

Si noti l'uso di ENTRAMBE le proprietà calcolate e memorizzate. Ho usato una proprietà calcolata per evitare di impostare due volte lo stesso valore (per evitare che accadano cose brutte!); Ho usato willSet e didSet per inoltrare le notifiche a viewController (vedere la documentazione e le informazioni di UIViewController sui contenitori viewController)

Spero che questo aiuti, e per favore qualcuno grida se ho fatto un errore da nessuna parte qui!


3
Perché non posso usare io uso didSet insieme a get e set ..?
Ben Sinclair,

//I can't see a way to 'stop' the value being set to the same controller - hence the computed property avviso scompare dopo che ho usato if let newViewController = _childVC { invece di if (_childVC) {
evfemist

5
get e set vengono utilizzati per creare una proprietà calcolata. Questi sono puramente metodi e non esiste alcuna memoria di backup (variabile di istanza). willSet e didSet servono per osservare le modifiche alle proprietà delle variabili memorizzate. Sotto il cofano, questi sono supportati da spazio di archiviazione, ma in Swift è tutto unito in uno.
user3675131

Nel tuo esempio 5, in get, penso che devi aggiungere if _childVC == nil { _childVC = something }e quindi return _childVC.
JW.ZG,

18

Questi sono chiamati osservatori di proprietà :

Gli osservatori di proprietà osservano e rispondono alle variazioni del valore di una proprietà. Gli osservatori di proprietà vengono chiamati ogni volta che viene impostato un valore di proprietà, anche se il nuovo valore è uguale al valore corrente della proprietà.

Estratto da: Apple Inc. "The Swift Programming Language". iBook. https://itun.es/ca/jEUH0.l

Ho il sospetto che consenta le cose che tradizionalmente faremmo con KVO come l'associazione di dati con elementi dell'interfaccia utente o l'attivazione di effetti collaterali della modifica di una proprietà, l'attivazione di un processo di sincronizzazione, l'elaborazione in background, ecc. Ecc.


16

NOTA

willSete gli didSetosservatori non vengono chiamati quando una proprietà è impostata in un inizializzatore prima che avvenga la delega


16

È inoltre possibile utilizzare il didSetper impostare la variabile su un valore diverso. Ciò non causa la richiamata dell'osservatore come indicato nella Guida alle proprietà . Ad esempio, è utile quando si desidera limitare il valore come di seguito:

let minValue = 1

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

value = -10 // value is minValue now.

10

Le molte risposte esistenti ben scritte coprono bene la domanda, ma menzionerò, in alcuni dettagli, un'aggiunta che credo valga la pena di essere trattata.


Gli osservatori proprietà willSete didSetpossono essere utilizzati per chiamare delegati, ad esempio, per le proprietà di classe che vengono sempre aggiornate solo dall'interazione dell'utente, ma dove si desidera evitare di chiamare il delegato all'inizializzazione dell'oggetto.

Citerò il commento di Klaas votato alla risposta accettata:

Gli osservatori willSet e didSet non vengono chiamati quando una proprietà viene inizializzata per la prima volta. Vengono chiamati solo quando il valore della proprietà è impostato al di fuori di un contesto di inizializzazione.

Questo è piuttosto accurato in quanto significa, ad esempio, la didSetproprietà è una buona scelta di punto di avvio per callback e funzioni delegate, per le tue classi personalizzate.

Ad esempio, si consideri un oggetto di controllo utente personalizzato, con alcune proprietà chiave value(ad es. Posizione nel controllo di classificazione), implementato come sottoclasse di 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.
}

Dopo di che le funzioni del delegato possono essere utilizzate, ad esempio, in alcuni controller di visualizzazione per osservare le modifiche chiave nel modello CustomViewController, proprio come se si utilizzassero le funzioni delegate intrinseche degli oggetti UITextFieldDelegatefor UITextField(ad es textFieldDidEndEditing(...).).

Per questo semplice esempio, utilizzare un callback delegato dalla didSetproprietà della classe valueper dire a un controller di visualizzazione che uno dei suoi punti vendita ha avuto un aggiornamento del modello associato:

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

Qui, la valueproprietà è stata incapsulata, ma in generale: in situazioni come queste, fai attenzione a non aggiornare la valueproprietà customUserControldell'oggetto nell'ambito della funzione delegata associata (qui:) didChangeValue()nel controller di visualizzazione, o finirai con ricorsione infinita.


4

Gli osservatori willSet e didSet per le proprietà ogni volta che alla proprietà viene assegnato un nuovo valore. Ciò è vero anche se il nuovo valore è uguale al valore corrente.

E nota che ha willSetbisogno di un nome di parametro per aggirare, d'altra parte, didSetno.

L'osservatore didSet viene chiamato dopo l'aggiornamento del valore della proprietà. Si confronta con il vecchio valore. Se il numero totale di passaggi è aumentato, viene stampato un messaggio per indicare quanti nuovi passaggi sono stati effettuati. L'osservatore didSet non fornisce un nome di parametro personalizzato per il vecchio valore e al suo posto viene utilizzato il nome predefinito di oldValue.


2

Getter e setter sono a volte troppo pesanti per essere implementati solo per osservare le variazioni del valore. Di solito questo richiede una gestione variabile temporanea extra e controlli extra, e vorrai evitare anche quei piccoli lavori se scrivi centinaia di getter e setter. Queste cose sono per la situazione.


1
Stai dicendo che c'è un vantaggio in termini di prestazioni nell'uso willSete didSetrispetto al codice setter equivalente? Sembra un'affermazione audace.
zneak,

1
@zneak Ho usato una parola sbagliata. Sto sostenendo lo sforzo del programmatore, non i costi di elaborazione.
Eonil,

1

Nella tua classe (base), willSete didSetsono abbastanza ridondanti , poiché potresti invece definire una proprietà calcolata (cioè metodi get e set) che accede a _propertyVariablee fa il pre e post-processo desiderati .

Se, invece , si sovrascrive una classe in cui la proprietà è già definito , poi il willSete didSetsono utili e non ridondante!


1

Una cosa in cui didSetè davvero utile è quando si utilizzano punti vendita per aggiungere ulteriore configurazione.

@IBOutlet weak var loginOrSignupButton: UIButton! {
  didSet {
        let title = NSLocalizedString("signup_required_button")
        loginOrSignupButton.setTitle(title, for: .normal)
        loginOrSignupButton.setTitle(title, for: .highlighted)
  }

o usando willSet ha senso alcuni effetti su questi metodi di outlet, non è vero?
elia,

-5

Non conosco C #, ma con un po 'di congetture penso di capire cosa

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

lo fa. Sembra molto simile a quello che hai in Swift, ma non è lo stesso: in Swift non hai il getFooe setFoo. Questa non è una piccola differenza: significa che non hai spazio di archiviazione sottostante per il tuo valore.

Swift ha proprietà memorizzate e calcolate.

Una proprietà calcolata ha gete può avere set(se è scrivibile). Ma il codice nel getter e nel setter, se devono effettivamente archiviare alcuni dati, deve farlo in altre proprietà. Non esiste memoria di backup.

Una proprietà memorizzata, d'altra parte, ha una memoria di supporto. Ma non ha gete set. Invece ha willSete didSetche è possibile utilizzare per osservare le variazioni delle variabili e, infine, innescare effetti collaterali e / o modificare il valore memorizzato. Non hai willSete didSetper le proprietà calcolate e non ti servono perché per le proprietà calcolate puoi usare il codice setper controllare le modifiche.


Questo è l'esempio di Swift. getFooe setFoosono semplici segnaposto per qualsiasi cosa tu voglia fare dai getter e dai setter. C # non ne ha nemmeno bisogno. (Mi mancavano alcune sottigliezze sintattiche come avevo chiesto prima di avere accesso al compilatore.)
zneak,

1
Oh va bene. Ma il punto importante è che una proprietà calcolata NON ha un archivio sottostante. Vedi anche la mia altra risposta: stackoverflow.com/a/24052566/574590
File analogico
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.