Quel est le but de willSet et didSet dans Swift?

265

Swift a une syntaxe de déclaration de propriété très similaire à C #:

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

Cependant, il a aussi willSetet didSetactions. Ceux-ci sont appelés respectivement avant et après l'appel du setter. Quel est leur objectif, étant donné que vous pouvez simplement avoir le même code à l'intérieur du setter?

zneak
la source
11
Personnellement, je n'aime pas beaucoup de réponses ici. Ils descendent trop dans la syntaxe. Les différences concernent davantage la sémantique et la lisibilité du code. La propriété calculée ( get& set) doit essentiellement avoir une propriété calculée sur la base d'une autre propriété, par exemple la conversion d'une étiquette texten une année Int. didSet& willSetsont là pour dire ... hé cette valeur a été définie, maintenant faisons ceci par exemple Notre dataSource a été mise à jour ... alors rechargeons la tableView afin qu'elle inclue de nouvelles lignes. Pour un autre exemple, voir la réponse de dfri sur la façon d'appeler les déléguésdidSet
Honey

Réponses:

324

Le point semble être que, parfois, vous avez besoin d'une propriété qui a un stockage automatique et un certain comportement, par exemple pour notifier à d'autres objets que la propriété vient d'être modifiée. Lorsque tout ce que vous avez est get/ set, vous avez besoin d'un autre champ pour contenir la valeur. Avec willSetet didSet, vous pouvez agir lorsque la valeur est modifiée sans avoir besoin d'un autre champ. Par exemple, dans cet exemple:

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

myPropertyimprime son ancienne et sa nouvelle valeur à chaque modification. Avec juste des getters et des setters, j'aurais besoin de ça à la place:

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

Donc willSetet didSetreprésentent une économie de quelques lignes, et moins de bruit dans la liste des champs.

zneak
la source
248
Attention: willSetet didSetne sont pas appelés lorsque vous définissez la propriété à partir d'une méthode init comme le note 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
Mais ils semblent être appelés sur une propriété de tableau lors de cette opération: myArrayProperty.removeAtIndex(myIndex)... Pas prévu.
Andreas
4
Vous pouvez encapsuler l'affectation dans une instruction defer {} dans l'initialiseur, ce qui provoque l'appel des méthodes willSet et didSet lorsque la portée de l'initialiseur est fermée. Je ne le recommande pas nécessairement, je dis simplement que c'est possible. L'une des conséquences est que cela ne fonctionne que si vous déclarez la propriété facultative, car elle n'est pas strictement initialisée à partir de l'initialiseur.
Marmoy
Veuillez expliquer ci-dessous la ligne. Je ne reçois pas, cette méthode ou variable var propertyChangedListener: (Int, Int) -> Void = {println ("La valeur de myProperty est passée de ($ 0) à ($ 1)")}
Vikash Rajput
L'initialisation des propriétés sur la même ligne n'est PAS prise en charge dans Swift 3. Vous devez modifier la réponse pour qu'elle soit conforme à Swift 3.
Ramazan Polat
149

Ma compréhension est que set et get sont pour les propriétés calculées (pas de support des propriétés stockées )

si vous venez d'un Objective-C sans oublier que les conventions de dénomination ont changé. Dans Swift, une variable iVar ou instance est nommée propriété stockée

Exemple 1 (propriété en lecture seule) - avec avertissement:

var test : Int {
    get {
        return test
    }
}

Cela se traduira par un avertissement car il en résulte un appel de fonction récursive (le getter appelle lui-même). L'avertissement dans ce cas est "Tentative de modification de" test "dans son propre getter".

Exemple 2. Lecture / écriture conditionnelle - avec avertissement

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

Problème similaire - vous ne pouvez pas le faire car il appelle récursivement le setter. Notez également que ce code ne se plaindra d'aucun initialiseur car il n'y a pas de propriété stockée à initialiser .

Exemple 3. Propriété calculée en lecture / écriture - avec magasin de sauvegarde

Voici un modèle qui permet le réglage conditionnel d'une propriété stockée réelle

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

Remarque Les données réelles sont appelées _test (bien qu'il puisse s'agir de n'importe quelle donnée ou combinaison de données) Notez également la nécessité de fournir une valeur initiale (vous devez également utiliser une méthode init) car _test est en fait une variable d'instance

Exemple 4. Utilisation de will et 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
        }
    }
}

Ici, nous verrons willSet et didSet intercepter un changement dans une propriété stockée réelle. Ceci est utile pour l'envoi de notifications, la synchronisation etc ... (voir exemple ci-dessous)

Exemple 5. Exemple concret - Conteneur 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
        }
    }
}

Notez l'utilisation des DEUX propriétés calculées et stockées. J'ai utilisé une propriété calculée pour éviter de définir deux fois la même valeur (pour éviter que de mauvaises choses ne se produisent!); J'ai utilisé willSet et didSet pour transmettre des notifications aux viewControllers (voir la documentation UIViewController et les informations sur les conteneurs viewController)

J'espère que cela aide, et je vous en prie, criez si j'ai fait une erreur ici!

user3675131
la source
3
Pourquoi ne puis-je pas utiliser didSet avec get et set ..?
Ben Sinclair
//I can't see a way to 'stop' the value being set to the same controller - hence the computed property l'avertissement disparaît après que j'aie utilisé à la if let newViewController = _childVC { place de if (_childVC) {
evfemist
5
get et set sont utilisés pour créer une propriété calculée. Ce sont purement des méthodes, et il n'y a pas de stockage de sauvegarde (variable d'instance). willSet et didSet permettent d'observer les modifications apportées aux propriétés des variables stockées. Sous le capot, ceux-ci sont soutenus par le stockage, mais dans Swift, tout est fusionné en un seul.
user3675131
Dans votre exemple 5, dans get, je pense que vous devez ajouter if _childVC == nil { _childVC = something }et ensuite return _childVC.
JW.ZG
18

Ceux-ci sont appelés observateurs immobiliers :

Les observateurs immobiliers observent et réagissent aux changements de valeur d'une propriété. Des observateurs de propriétés sont appelés chaque fois que la valeur d'une propriété est définie, même si la nouvelle valeur est identique à la valeur actuelle de la propriété.

Extrait de: Apple Inc. «The Swift Programming Language». iBooks. https://itun.es/ca/jEUH0.l

Je soupçonne que c'est pour permettre des choses que nous ferions traditionnellement avec KVO telles que la liaison de données avec des éléments d'interface utilisateur, ou le déclenchement d'effets secondaires du changement d'une propriété, le déclenchement d'un processus de synchronisation, le traitement en arrière-plan, etc., etc.

Sébastien Martin
la source
16

REMARQUE

willSetet les didSetobservateurs ne sont pas appelés lorsqu'une propriété est définie dans un initialiseur avant la délégation a lieu

Bartłomiej Semańczyk
la source
16

Vous pouvez également utiliser le didSetpour définir la variable sur une valeur différente. Cela ne provoque pas l'appel de l'observateur comme indiqué dans le guide des propriétés . Par exemple, il est utile lorsque vous souhaitez limiter la valeur comme ci-dessous:

let minValue = 1

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

value = -10 // value is minValue now.
knshn
la source
10

Les nombreuses réponses existantes bien écrites couvrent bien la question, mais je mentionnerai, en détail, un ajout qui, je crois, mérite d'être couvert.


Les observateurs de propriétés willSetet didSetpeuvent être utilisés pour appeler des délégués, par exemple, pour des propriétés de classe qui ne sont jamais mises à jour que par interaction de l'utilisateur, mais où vous voulez éviter d'appeler le délégué lors de l'initialisation de l'objet.

Je citerai le commentaire voté par Klaas à la réponse acceptée:

Les observateurs willSet et didSet ne sont pas appelés lors de la première initialisation d'une propriété. Ils ne sont appelés que lorsque la valeur de la propriété est définie en dehors d'un contexte d'initialisation.

Ceci est assez soigné car cela signifie par exemple que la didSetpropriété est un bon choix de point de lancement pour les fonctions et rappels délégués, pour vos propres classes personnalisées.

Par exemple, considérons un objet de contrôle utilisateur personnalisé, avec une propriété clé value(par exemple, la position dans le contrôle de notation), implémenté comme une sous-classe de 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.
}

Après quoi, vos fonctions de délégué peuvent être utilisées dans, disons, certains contrôleurs de vue pour observer les changements clés dans le modèle CustomViewController, tout comme vous utiliseriez les fonctions de délégué inhérentes UITextFieldDelegateaux UITextFieldobjets for (par exemple textFieldDidEndEditing(...)).

Pour cet exemple simple, utilisez un rappel délégué à partir didSetde la propriété de classe valuepour indiquer à un contrôleur de vue que l'une de ses sorties a eu une mise à jour de modèle associée:

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

Ici, la valuepropriété a été encapsulée, mais généralement: dans des situations comme celles-ci, veillez à ne pas mettre à jour la valuepropriété de l' customUserControlobjet dans la portée de la fonction déléguée associée (ici:) didChangeValue()dans le contrôleur de vue, ou vous vous retrouverez avec récursion infinie.

dfri
la source
4

Les observateurs willSet et didSet des propriétés chaque fois qu'une nouvelle valeur est affectée à la propriété. Cela est vrai même si la nouvelle valeur est identique à la valeur actuelle.

Et notez que cela willSetnécessite un nom de paramètre pour contourner, en revanche, didSetne le fait pas.

L'observateur didSet est appelé après la mise à jour de la valeur de la propriété. Il se compare à l'ancienne valeur. Si le nombre total d'étapes a augmenté, un message est imprimé pour indiquer combien de nouvelles étapes ont été effectuées. L'observateur didSet ne fournit pas de nom de paramètre personnalisé pour l'ancienne valeur et le nom par défaut de oldValue est utilisé à la place.

Zigii Wong
la source
2

Getter et setter sont parfois trop lourds à implémenter juste pour observer les changements de valeur appropriés. Habituellement, cela nécessite une gestion supplémentaire des variables temporaires et des vérifications supplémentaires, et vous voudrez éviter même ce travail minime si vous écrivez des centaines de getters et setters. Ces trucs sont pour la situation.

Eonil
la source
1
Êtes-vous en train de dire qu'il y a un avantage en termes de performances à utiliser willSetet didSetpar rapport à un code setter équivalent? Cela semble être une affirmation audacieuse.
zneak
1
@zneak J'ai utilisé un mauvais mot. Je réclame l'effort du programmeur, pas le coût de traitement.
Eonil
1

Dans votre propre classe (de base), willSetet didSetsont assez redondants , car vous pouvez plutôt définir une propriété calculée (c'est-à-dire les méthodes get et set) qui accède à a _propertyVariableet effectue les pré et post-évaluation souhaitées .

Si, cependant , vous surchargez une classe où la propriété est déjà définie , alors les willSetet didSetsont utiles et non redondants!

ragnarius
la source
1

Une chose didSetest vraiment pratique lorsque vous utilisez des prises pour ajouter une configuration supplémentaire.

@IBOutlet weak var loginOrSignupButton: UIButton! {
  didSet {
        let title = NSLocalizedString("signup_required_button")
        loginOrSignupButton.setTitle(title, for: .normal)
        loginOrSignupButton.setTitle(title, for: .highlighted)
  }
orkoden
la source
ou en utilisant willSet a un sens certains effets sur les méthodes de cette sortie, n'est-ce pas?
elia
-5

Je ne connais pas C #, mais avec un peu de conjectures je pense que je comprends

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

Est-ce que. Cela ressemble beaucoup à ce que vous avez dans Swift, mais ce n'est pas la même chose: dans Swift, vous n'avez pas le getFooet setFoo. Ce n'est pas une petite différence: cela signifie que vous n'avez pas de stockage sous-jacent pour votre valeur.

Swift a stocké et calculé des propriétés.

Une propriété calculée a getet peut avoir set(si elle est accessible en écriture). Mais le code dans le getter et le setter, s'il doit réellement stocker des données, doit le faire dans d' autres propriétés. Il n'y a pas de stockage de sauvegarde.

Une propriété stockée, d'autre part, a un stockage de sauvegarde. Mais il ne pas avoir getet set. Au lieu de cela, il a willSetet didSetque vous pouvez utiliser pour observer les changements de variables et, éventuellement, déclencher des effets secondaires et / ou modifier la valeur stockée. Vous n'avez pas willSetet didSetpour les propriétés calculées, et vous n'en avez pas besoin car pour les propriétés calculées, vous pouvez utiliser le code dans setpour contrôler les modifications.

Fichier analogique
la source
Ceci est l'exemple Swift. getFooet setFoosont de simples espaces réservés pour tout ce que vous souhaitez que les getters et les setters fassent. C # n'en a pas besoin non plus. (J'ai manqué quelques subtilités syntaxiques comme je l'ai demandé avant d'avoir accès au compilateur.)
zneak
1
Ah d'accord. Mais le point important est qu'une propriété calculée n'a PAS de stockage sous-jacent. Voir aussi mon autre réponse: stackoverflow.com/a/24052566/574590
Analog File