¿Cuál es el propósito de willSet y didSet en Swift?

265

Swift tiene una sintaxis de declaración de propiedad muy similar a la de C #:

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

Sin embargo, también tiene willSety didSetacciones. Estos se llaman antes y después de llamar al setter, respectivamente. ¿Cuál es su propósito, teniendo en cuenta que podría tener el mismo código dentro del setter?

zneak
fuente
11
Personalmente no me gustan muchas respuestas aquí. Bajan demasiado en la sintaxis. Las diferencias son más sobre semántica y readiblity de código. La propiedad calculada ( get& set) básicamente consiste en calcular una propiedad basada en otra propiedad, por ejemplo, convertir una etiqueta texten un año Int. didSety willSetestamos ahí para decir ... hey, este valor se estableció, ahora hagamos esto, por ejemplo, Nuestro dataSource se actualizó ... así que volvamos a cargar el TableView para que incluya nuevas filas. Para otro ejemplo, vea la respuesta de dfri sobre cómo llamar a los delegados endidSet
Honey

Respuestas:

324

El punto parece ser que a veces, necesita una propiedad que tenga almacenamiento automático y algún comportamiento, por ejemplo, para notificar a otros objetos que la propiedad acaba de cambiar. Cuando todo lo que tiene es get/ set, necesita otro campo para mantener el valor. Con willSety didSet, puede tomar medidas cuando se modifica el valor sin necesidad de otro campo. Por ejemplo, en ese ejemplo:

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

myPropertyimprime su valor antiguo y nuevo cada vez que se modifica. Con solo getters y setters, necesitaría esto en su lugar:

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

Así willSety didSetrepresentar una economía de un par de líneas, y menos ruido en la lista de campos.

zneak
fuente
248
Atención: willSety didSetno se llama cuando configura la propiedad desde un método init como Apple observa: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
44
Pero parece que se les llama en una propiedad de matriz al hacer esto: myArrayProperty.removeAtIndex(myIndex)... No se espera.
Andreas
44
Puede ajustar la asignación en una declaración diferida {} dentro del iniciador que hace que se invoquen los métodos willSet y didSet cuando se sale del ámbito del inicializador. No lo recomiendo necesariamente, solo digo que es posible. Una de las consecuencias es que solo funciona si declara que la propiedad es opcional, ya que no se inicializa estrictamente desde el inicializador.
Marmoy
Por favor explique debajo de la línea. No estoy obteniendo, es este método o variable var propertyChangedListener: (Int, Int) -> Void = {println ("El valor de myProperty ha cambiado de ($ 0) a ($ 1)")}
Vikash Rajput
Las propiedades de inicialización en la misma línea NO son compatibles con Swift 3. Debe cambiar la respuesta para conformar Swift 3.
Ramazan Polat
149

Entiendo que set y get son para propiedades calculadas (sin respaldo de propiedades almacenadas )

si viene de un Objective-C, tenga en cuenta que las convenciones de nomenclatura han cambiado. En Swift, una variable iVar o instancia se denomina propiedad almacenada

Ejemplo 1 (propiedad de solo lectura) - con advertencia:

var test : Int {
    get {
        return test
    }
}

Esto dará como resultado una advertencia porque esto dará como resultado una llamada de función recursiva (el getter se llama a sí mismo). La advertencia en este caso es "Intentar modificar 'prueba' dentro de su propio getter".

Ejemplo 2. Lectura / escritura condicional - con advertencia

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 similar: no puede hacer esto ya que llama de forma recursiva al setter. Además, tenga en cuenta que este código no se quejará de que no hay inicializadores, ya que no hay propiedades almacenadas para inicializar .

Ejemplo 3. propiedad calculada de lectura / escritura - con almacén de respaldo

Aquí hay un patrón que permite la configuración condicional de una propiedad almacenada real

//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 Los datos reales se denominan _test (aunque podría ser cualquier dato o combinación de datos). Tenga en cuenta también la necesidad de proporcionar un valor inicial (alternativamente, debe usar un método init) porque _test es en realidad una variable de instancia

Ejemplo 4. Usando will y 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
        }
    }
}

Aquí vemos willSet y didSet interceptando un cambio en una propiedad almacenada real. Esto es útil para enviar notificaciones, sincronización, etc. (ver ejemplo a continuación)

Ejemplo 5. Ejemplo concreto: Contenedor 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
        }
    }
}

Tenga en cuenta el uso de AMBAS propiedades calculadas y almacenadas. He usado una propiedad calculada para evitar establecer el mismo valor dos veces (¡para evitar que sucedan cosas malas!); He usado willSet e didSet para reenviar notificaciones a viewControllers (consulte la documentación e información de UIViewController en los contenedores de viewController)

¡Espero que esto ayude, y por favor alguien grite si he cometido un error en algún lugar aquí!

usuario3675131
fuente
3
¿Por qué no puedo usar que utilizo didSet junto con get y set ..?
Ben Sinclair
//I can't see a way to 'stop' the value being set to the same controller - hence the computed property advertencia desaparece después de usar en if let newViewController = _childVC { lugar de if (_childVC) {
evfemist
55
get y set se usan para crear una propiedad calculada. Estos son puramente métodos, y no hay almacenamiento de respaldo (variable de instancia). willSet y didSet son para observar cambios en las propiedades de variables almacenadas. Debajo del capó, estos están respaldados por el almacenamiento, pero en Swift todo se combina en uno.
user3675131
En su ejemplo 5, en get, creo que necesita agregar if _childVC == nil { _childVC = something }y luego return _childVC.
JW.ZG
18

Estos se llaman Observadores de propiedades :

Los observadores de propiedades observan y responden a los cambios en el valor de una propiedad. Se llama a los observadores de propiedades cada vez que se establece el valor de una propiedad, incluso si el nuevo valor es el mismo que el valor actual de la propiedad.

Extracto de: Apple Inc. "El lenguaje de programación Swift". iBooks https://itun.es/ca/jEUH0.l

Sospecho que es para permitir cosas que tradicionalmente haríamos con KVO , como el enlace de datos con elementos de la interfaz de usuario, o desencadenar efectos secundarios de cambiar una propiedad, desencadenar un proceso de sincronización, procesamiento en segundo plano, etc.

Sebastien Martin
fuente
16

NOTA

willSety didSetno se llama a los observadores cuando se establece una propiedad en un inicializador antes de que tenga lugar la delegación

Bartłomiej Semańczyk
fuente
16

También puede usar el didSetpara establecer la variable en un valor diferente. Esto no hace que se vuelva a llamar al observador como se indica en la guía de propiedades . Por ejemplo, es útil cuando desea limitar el valor de la siguiente manera:

let minValue = 1

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

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

Las muchas respuestas existentes bien escritas cubren bien la pregunta, pero mencionaré, con cierto detalle, una adición que creo que vale la pena cubrir.


Los observadores de propiedades willSety didSetse pueden usar para llamar a delegados, por ejemplo, para propiedades de clase que solo se actualizan por interacción del usuario, pero donde desea evitar llamar al delegado en la inicialización del objeto.

Citaré el comentario votado de Klaas a la respuesta aceptada:

Los observadores willSet y didSet no son llamados cuando una propiedad se inicializa por primera vez. Solo se llaman cuando el valor de la propiedad se establece fuera de un contexto de inicialización.

Esto es bastante bueno, ya que significa, por ejemplo, que la didSetpropiedad es una buena opción de punto de inicio para delegar devoluciones de llamada y funciones, para sus propias clases personalizadas.

Como ejemplo, considere algún objeto de control de usuario personalizado, con alguna propiedad clave value(por ejemplo, posición en el control de calificación), implementado como una subclase 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.
}

Después de lo cual sus funciones de delegado se pueden usar en, digamos, algún controlador de vista para observar los cambios clave en el modelo CustomViewController, al igual que usaría las funciones de delegado inherentes de los objetos UITextFieldDelegatefor UITextField(por ejemplo textFieldDidEndEditing(...)).

Para este ejemplo simple, use una devolución de llamada delegada didSetde la propiedad de la clase valuepara decirle a un controlador de vista que una de sus salidas ha tenido una actualización de modelo asociada:

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

Aquí, la valuepropiedad se ha encapsulado, pero en general: en situaciones como estas, tenga cuidado de no actualizar la valuepropiedad del customUserControlobjeto en el ámbito de la función delegada asociada (aquí:) didChangeValue()en el controlador de vista, o terminará con recursión infinita

dfri
fuente
4

Los observadores willSet y didSet de las propiedades siempre que se les asigne un nuevo valor. Esto es cierto incluso si el nuevo valor es el mismo que el valor actual.

Y tenga en cuenta que willSetnecesita un nombre de parámetro para evitar, por otro lado, didSetno.

Se llama al observador didSet después de actualizar el valor de la propiedad. Se compara con el valor anterior. Si el número total de pasos ha aumentado, se imprime un mensaje para indicar cuántos pasos nuevos se han tomado. El observador didSet no proporciona un nombre de parámetro personalizado para el valor anterior, y en su lugar se usa el nombre predeterminado de oldValue.

Zigii Wong
fuente
2

Getter y setter a veces son demasiado pesados ​​para implementar solo para observar los cambios de valor adecuados. Por lo general, esto requiere un manejo variable temporal adicional y controles adicionales, y querrá evitar incluso esa pequeña mano de obra si escribe cientos de captadores y establecedores. Estas cosas son para la situación.

Eonil
fuente
1
¿Estás diciendo que hay una ventaja de rendimiento al usar willSety didSetcontra el código de establecimiento equivalente? Esto parece una afirmación audaz.
zneak
1
@zneak Usé una palabra incorrecta. Estoy reclamando el esfuerzo del programador, no el costo de procesamiento.
Eonil
1

En su propia clase (base), willSety didSetson bastante redundantes , ya que en su lugar podría definir una propiedad calculada (es decir, métodos get y set ) que acceden ay_propertyVariable hacen el pre y post prospeso deseado .

Si, sin embargo , se reemplaza una clase donde la propiedad está ya definido , entonces el willSety didSetson útiles y no redundante!

ragnarius
fuente
1

Una cosa que didSetes realmente útil es cuando usa puntos de venta para agregar configuración adicional.

@IBOutlet weak var loginOrSignupButton: UIButton! {
  didSet {
        let title = NSLocalizedString("signup_required_button")
        loginOrSignupButton.setTitle(title, for: .normal)
        loginOrSignupButton.setTitle(title, for: .highlighted)
  }
orkoden
fuente
o usando willSet tiene sentido algunos efectos en estos métodos de salida, ¿no es así?
elia
-5

No sé C #, pero con un poco de conjeturas creo que entiendo qué

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

hace. Se ve muy similar a lo que tiene en Swift, pero no es lo mismo: en Swift no tiene el getFooy setFoo. Esa no es una pequeña diferencia: significa que no tiene ningún almacenamiento subyacente para su valor.

Swift ha almacenado y calculado propiedades.

Una propiedad calculada tiene gety puede tener set(si se puede escribir). Pero el código en getter y setter, si necesitan almacenar algunos datos, debe hacerlo en otras propiedades. No hay almacenamiento de respaldo.

Una propiedad almacenada, por otro lado, tiene almacenamiento de respaldo. Pero no tiene gety set. En cambio, sí, willSety didSetpuede usarlo para observar cambios variables y, eventualmente, desencadenar efectos secundarios y / o modificar el valor almacenado. No tiene willSety didSetpara propiedades calculadas, y no las necesita porque para propiedades calculadas puede usar el código setpara controlar los cambios.

Archivo Analógico
fuente
Este es el ejemplo de Swift. getFooy setFooson simples marcadores de posición para lo que quiera que hagan los captadores y establecedores. C # tampoco los necesita. (
Eché de
1
Oh ok Pero el punto importante es que una propiedad calculada NO tiene un almacenamiento subyacente. Vea también mi otra respuesta: stackoverflow.com/a/24052566/574590
Archivo analógico