Qual é o objetivo do willSet e didSet no Swift?

265

Swift tem uma sintaxe de declaração de propriedade muito semelhante à de C #:

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

No entanto, também possui willSete didSetações. Eles são chamados antes e depois que o setter é chamado, respectivamente. Qual é o propósito deles, considerando que você poderia ter o mesmo código dentro do setter?

zneak
fonte
11
Eu pessoalmente não gosto de muitas respostas aqui. Eles entram muito na sintaxe. As diferenças são mais sobre semântica e legibilidade de código. Propriedade computada ( get& set) consiste basicamente em ter uma propriedade calculada com base em outra propriedade, por exemplo, converter um rótulo textem um ano Int. didSet& willSetexistem para dizer ... ei, esse valor foi definido, agora vamos fazer isso, por exemplo, Nosso dataSource foi atualizado ... então vamos recarregar o tableView para incluir novas linhas. Para outro exemplo ver a resposta de DFRI sobre como chamar os delegados emdidSet
Mel

Respostas:

324

O ponto parece ser que, às vezes, você precisa de uma propriedade com armazenamento automático e algum comportamento, por exemplo, para notificar outros objetos que a propriedade acabou de ser alterada. Quando tudo o que você tem é get/ set, você precisa de outro campo para armazenar o valor. Com willSete didSet, você pode executar uma ação quando o valor é modificado sem precisar de outro campo. Por exemplo, nesse exemplo:

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

myPropertyimprime seu valor antigo e novo toda vez que é modificado. Com apenas getters e setters, eu precisaria disso:

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

Assim, willSete didSetrepresentam uma economia de um par de linhas, e menos ruído na lista de campos.

zneak
fonte
248
Atenção: willSete didSetnão são chamados quando você define a propriedade de dentro de um método init, como observa a 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
Mas eles parecem ser chamados em uma propriedade de matriz ao fazer isso: myArrayProperty.removeAtIndex(myIndex)... Não é esperado.
Andreas
4
Você pode agrupar a atribuição em uma instrução defer {} no inicializador, o que faz com que os métodos willSet e didSet sejam chamados quando o escopo do inicializador for encerrado. Não estou necessariamente recomendando, apenas dizendo que é possível. Uma das conseqüências é que ela só funciona se você declarar a propriedade opcional, pois ela não está sendo estritamente inicializada pelo inicializador.
Marmoy
Por favor, explique abaixo da linha. Não estou recebendo, esse método ou variável var propertyChangedListener: (Int, Int) -> Void = {println ("O valor de myProperty mudou de ($ 0) para ($ 1)"))
Vikash Rajput
Inicialização propriedades em mesma linha não é suportado no Swift 3. Você deve alterar a resposta para conformar rápida 3.
Ramazan Polat
149

Meu entendimento é que set e get são para propriedades computadas (sem suporte de propriedades armazenadas )

se você for de um Objective-C, tenha em mente que as convenções de nomenclatura foram alteradas. No Swift, uma iVar ou variável de instância é denominada propriedade armazenada

Exemplo 1 (propriedade somente leitura) - com aviso:

var test : Int {
    get {
        return test
    }
}

Isso resultará em um aviso, pois resultará em uma chamada de função recursiva (o getter chama a si mesmo). O aviso neste caso é "Tentativa de modificar 'teste' dentro de seu próprio getter".

Exemplo 2. Leitura / gravação condicional - com aviso

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 semelhante - você não pode fazer isso , pois está chamando recursivamente o setter. Além disso, observe que este código não irá reclamar de nenhum inicializador, pois não há propriedade armazenada para inicializar .

Exemplo 3. Propriedade Computada de Leitura / Gravação - com Armazenamento de Suporte

Aqui está um padrão que permite a configuração condicional de uma propriedade armazenada 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 Os dados reais são chamados _test (embora possam ser quaisquer dados ou combinação de dados). Observe também a necessidade de fornecer um valor inicial (como alternativa, você precisa usar um método init) porque _test é na verdade uma variável de instância

Exemplo 4. Usando vontade e definir

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

Aqui vemos willSet e didSet interceptando uma alteração em uma propriedade armazenada real. Isso é útil para enviar notificações, sincronização etc ... (veja o exemplo abaixo)

Exemplo 5. Exemplo concreto - container 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
        }
    }
}

Observe o uso de ambas as propriedades computadas e armazenadas. Eu usei uma propriedade computada para evitar definir o mesmo valor duas vezes (para evitar que coisas ruins aconteçam!); Eu usei o willSet e o didSet para encaminhar notificações para o viewControllers (consulte a documentação do UIViewController e informações sobre os contêineres do viewController)

Espero que isso ajude, e por favor alguém grite se eu cometi um erro em qualquer lugar aqui!

user3675131
fonte
3
Por que não posso usar eu uso didSet junto com 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 aviso desaparecer depois que eu usei em if let newViewController = _childVC { vez de if (_childVC) {
evfemist
5
get e set são usados ​​para criar uma propriedade computada. Esses são métodos puramente e não há armazenamento de backup (variável de instância). willSet e didSet são para observar alterações nas propriedades da variável armazenada. Sob o capô, eles são apoiados por armazenamento, mas no Swift tudo é fundido em um.
user3675131
No seu exemplo 5, em get, acho que você precisa adicionar if _childVC == nil { _childVC = something }e depois return _childVC.
JW.ZG
18

Estes são chamados Observadores de Propriedade :

Observadores de propriedades observam e respondem a alterações no valor de uma propriedade. Observadores de propriedades são chamados toda vez que o valor de uma propriedade é definido, mesmo que o novo valor seja o mesmo que o valor atual da propriedade.

Trecho de: Apple Inc. “A linguagem de programação Swift”. iBooks. https://itun.es/ca/jEUH0.l

Eu suspeito que é para permitir coisas que tradicionalmente faríamos com o KVO , como ligação de dados com elementos da interface do usuário ou desencadear efeitos colaterais de alterar uma propriedade, desencadear um processo de sincronização, processamento em segundo plano, etc.

Sebastien Martin
fonte
16

NOTA

willSete os didSetobservadores não são chamados quando uma propriedade é definida em um inicializador antes da delegação

Bartłomiej Semańczyk
fonte
16

Você também pode usar o didSetpara definir a variável com um valor diferente. Isso não faz com que o observador seja chamado novamente, conforme indicado no guia Propriedades . Por exemplo, é útil quando você deseja limitar o valor conforme abaixo:

let minValue = 1

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

value = -10 // value is minValue now.
Knshn
fonte
10

As muitas respostas existentes bem escritas cobrem bem a pergunta, mas mencionarei, em alguns detalhes, uma adição que acredito que vale a pena abordar.


Os observadores willSete didSetproperty podem ser usados ​​para chamar delegados, por exemplo, para propriedades de classe que são sempre atualizadas apenas pela interação do usuário, mas onde você deseja evitar chamar o delegado na inicialização do objeto.

Vou citar o comentário votado por Klaas na resposta aceita:

Os observadores willSet e didSet não são chamados quando uma propriedade é inicializada. Eles são chamados apenas quando o valor da propriedade é definido fora de um contexto de inicialização.

Isso é bastante interessante, pois significa, por exemplo, que a didSetpropriedade é uma boa opção de ponto de ativação para delegar retornos de chamada e funções, para suas próprias classes personalizadas.

Como exemplo, considere algum objeto de controle de usuário personalizado, com alguma propriedade-chave value(por exemplo, posição no controle de classificação), implementada como uma subclasse 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.
}

Depois das quais suas funções delegadas podem ser usadas, digamos, em algum controlador de exibição para observar as principais alterações no modelo CustomViewController, assim como você usaria as funções delegadas inerentes UITextFieldDelegateaos UITextFieldobjetos for (por exemplo textFieldDidEndEditing(...)).

Para este exemplo simples, use um retorno de chamada delegado da didSetpropriedade da classe valuepara informar ao controlador de exibição que uma de suas saídas teve atualização de modelo associada:

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

Aqui, a valuepropriedade foi encapsulada, mas geralmente: em situações como estas, tenha cuidado para não atualizar a valuepropriedade do customUserControlobjeto no escopo da função de delegado associada (aqui didChangeValue():) no controlador de exibição, ou você terminará com recursão infinita.

dfri
fonte
4

Os observadores willSet e didSet para as propriedades sempre que a propriedade recebe um novo valor. Isso é verdade mesmo se o novo valor for o mesmo que o valor atual.

E observe que willSetprecisa de um nome de parâmetro para contornar, por outro lado, didSetnão.

O observador didSet é chamado depois que o valor da propriedade é atualizado. Ele se compara ao valor antigo. Se o número total de etapas tiver aumentado, uma mensagem será impressa para indicar quantas novas etapas foram executadas. O observador didSet não fornece um nome de parâmetro personalizado para o valor antigo e o nome padrão de oldValue é usado.

Zigii Wong
fonte
2

Às vezes, o getter e o setter são pesados ​​demais para serem implementados apenas para observar as mudanças apropriadas nos valores. Geralmente, isso requer manipulação variável temporária extra e verificações extras, e você deve evitar até mesmo esse trabalho minúsculo se escrever centenas de getters e setters. Esses animais são para a situação.

Eonil
fonte
1
Você está dizendo que há uma vantagem de desempenho em usar willSetedidSet versus código setter equivalente? Parece uma afirmação ousada.
zneak
1
@ zneak eu usei a palavra errada. Estou reivindicando o esforço do programador, não o custo de processamento.
Eonil
1

Na sua própria classe (base), willSete didSeté bastante redundante , pois você pode definir uma propriedade calculada (por exemplo, métodos get e set) que acessa ae_propertyVariable faz a pré e pós-prosessing desejada .

Se, no entanto , você substituir uma classe em que a propriedade está definida , em seguida, o willSete didSetsão úteis e não redundante!

ragnarius
fonte
1

Uma coisa didSetrealmente útil é quando você usa tomadas para adicionar configurações adicionais.

@IBOutlet weak var loginOrSignupButton: UIButton! {
  didSet {
        let title = NSLocalizedString("signup_required_button")
        loginOrSignupButton.setTitle(title, for: .normal)
        loginOrSignupButton.setTitle(title, for: .highlighted)
  }
orkoden
fonte
ou usar o willSet faz sentido alguns efeitos sobre esses métodos de saída, não é?
Elia
-5

Eu não sei C #, mas com um pouco de adivinhação, acho que entendo o que

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

faz. Parece muito semelhante ao que você tem no Swift, mas não é o mesmo: no Swift você não tem o getFooe setFoo. Essa não é uma pequena diferença: significa que você não tem nenhum armazenamento subjacente pelo seu valor.

Swift possui propriedades armazenadas e computadas.

Uma propriedade computada possui gete pode ter set(se for gravável). Mas o código no getter e setter, se eles realmente precisam armazenar alguns dados, deve fazê-lo em outra propriedades. Não há armazenamento de backup.

Uma propriedade armazenada, por outro lado, possui armazenamento de backup. Mas não tem gete set. Em vez disso, possui willSete didSetpode ser usado para observar alterações variáveis ​​e, eventualmente, desencadear efeitos colaterais e / ou modificar o valor armazenado. Você não tem willSetedidSet para propriedades calculadas e não precisa delas porque, para propriedades calculadas, é possível usar o código setpara controlar as alterações.

Arquivo Analógico
fonte
Este é o exemplo do Swift. getFooe setFoosão espaços reservados simples para o que você deseja que os getters e setters façam. C # também não precisa deles. (I fez perder alguns sutilezas sintáticas como eu pedi antes que eu tivesse acesso ao compilador.)
zneak
1
Ah ok. Mas o ponto importante é que uma propriedade calculada NÃO possui um armazenamento subjacente. Veja também minha outra resposta: stackoverflow.com/a/24052566/574590
Arquivo analógico