在Swift中使用willSet和didSet的目的是什么?

时间:2014-06-03 02:32:38

标签: swift

Swift的属性声明语法非常类似于C#:

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

但是,它还有willSetdidSet个动作。这些在分别调用setter之前和之后调用。考虑到你可以在setter中使用相同的代码,它们的目的是什么?

11 个答案:

答案 0 :(得分:305)

关键似乎是,有时候,您需要一个具有自动存储某些行为的属性,例如通知其他对象该属性刚刚更改。当您拥有get / set时,您需要另一个字段来保存该值。使用willSetdidSet,您可以在修改值时执行操作,而无需其他字段。例如,在那个例子中:

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

myProperty每次修改时都会打印旧值和新值。只有吸气剂和制定者,我需要这样做:

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

所以willSetdidSet代表了几行经济,而且字段列表中的噪音更少。

答案 1 :(得分:143)

我的理解是computed properties的设置和获取(不是来自stored properties的支持)

如果您来自Objective-C,请记住命名约定已更改。在Swift中,iVar或实例变量名为存储属性

示例1(只读属性) - 带警告:

var test : Int {
    get {
        return test
    }
}

这将导致警告,因为这会导致递归函数调用(getter调用自身)。在这种情况下的警告是"尝试修改' test'在自己的吸气剂中#34;

示例2.条件读/写 - 带警告

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

类似的问题 - 你不能这样做因为它递归地调用了setter。 另请注意,此代码不会抱怨没有初始化者没有存储属性可以初始化

示例3.读/写计算属性 - 使用后备存储

这是一种允许条件设置实际存储属性的模式

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

注意实际数据称为_test(尽管它可以是任何数据或数据组合) 还要注意需要提供初始值(或者你需要使用init方法),因为_test实际上是一个实例变量

示例4.使用will和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
        }
    }
}

这里我们看到willSet和didSet拦截了实际存储属性的变化。 这对于发送通知,同步等非常有用...(参见下面的示例)

示例5.具体示例 - 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
        }
    }
}

注意使用BOTH计算和存储的属性。我使用了一个计算属性来防止设置两次相同的值(以避免发生不好的事情!);我使用了willSet和didSet将通知转发给viewControllers(参见UIViewController文档和viewController容器上的信息)

我希望这会有所帮助,如果我在这里任何地方犯了错误,请有人大声喊叫!

答案 2 :(得分:18)

这些被称为 Property Observers

  

财产观察员观察并回应房产的变化   值。每次物业的价值都会调用物业观察员   设置,即使新值与属性的当前值相同   值。

摘自:Apple Inc.“The Swift Programming Language。”iBooks。 https://itun.es/ca/jEUH0.l

我怀疑这是允许我们传统上使用KVO做的事情,例如与UI元素的数据绑定,或触发更改属性,触发同步过程,后台处理等的副作用等。< / p>

答案 3 :(得分:16)

您还可以使用didSet将变量设置为其他值。这不会导致观察者再次被调用,如Properties guide中所述。例如,当您想要将值限制为如下时,它非常有用:

let minValue = 1

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

value = -10 // value is minValue now.

答案 4 :(得分:15)

  

请注意

     在委派发生之前在初始化程序中设置属性时,不会调用

willSetdidSet个观察者

答案 5 :(得分:10)

许多写得很好的现有答案很好地涵盖了这个问题,但我会更详细地提到一个我认为值得报道的补充。

willSetdidSet属性观察器可用于调用委托,例如,用于仅通过用户交互更新的类属性,但是您希望避免在对象初始化时调用委托

我会引用Klaas对接受的答案进行评论:

  

当属性第一个时,不会调用willSet和didSet观察者   初始化。它们仅在设置属性值时调用   在初始化上下文之外。

这是非常整洁的,因为它意味着,例如didSet属性是委托回调和发起点的一个很好的选择。函数,用于您自己的自定义类。

作为示例,请考虑一些自定义用户控件对象,其中包含一些关键属性value(例如,在评级控件中的位置),作为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.
}

之后,您的委托函数可用于某些视图控制器,以观察CustomViewController模型中的关键更改,就像您使用UITextFieldDelegate的固有委托函数一样UITextField个对象(例如textFieldDidEndEditing(...))。

对于这个简单的示例,使用来自类属性didSet的{​​{1}}的委托回调来告诉视图控制器其中一个出口已经关联了模型更新:

value

这里,// 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 ... //} } 属性已被封装,但一般情况下:在这种情况下,请注意不要在关联范围内更新value对象的value属性在视图控制器中委托函数(这里:customUserControl),否则你最终会得到无限递归。

答案 6 :(得分:4)

  

每当为属性分配新值时,willSet和didSet属性的观察者。即使新值与当前值相同,也是如此。

请注意,<%@ Property ..... Type="**SchemaExplorer.???**" ... 需要一个参数名称才能解决,另一方面willSet则不需要。

  

更新属性值后调用didSet观察者。它与旧值进行比较。如果步骤总数增加,则会打印一条消息,指示已执行了多少新步骤。 didSet观察者不为旧值提供自定义参数名称,而是使用默认名称oldValue。

答案 7 :(得分:2)

Getter和setter有时太重,无法实现只是为了观察正确的值变化。通常这需要额外的临时变量处理和额外检查,如果你写了数百个getter和setter,你甚至想要避免那些微小的劳动。这些东西适用于这种情况。

答案 8 :(得分:1)

在您自己的(基础)课程中,willSetdidSet非常 reduntant ,因为您可以改为定义计算属性(即get-和set-方法)访问_propertyVariable并执行所需的前后

如果,但是,则覆盖属性已定义的类然后 willSet和{{1是有用而不是多余的!

答案 9 :(得分:1)

didSet非常方便的一件事是当您使用插座添加其他配置时。

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

答案 10 :(得分:-4)

我不知道C#,但有一点猜测,我想我明白了什么

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

一样。它看起来与你在Swift中的相似,但它不一样:在Swift中你没有getFoosetFoo。这没有什么区别:它意味着您的价值没有任何底层存储。

Swift存储并计算了属性。

计算属性有get,可能有set(如果它是可写的)。但是getter和setter中的代码,如果需要实际存储一些数据,必须在其他属性中执行。没有后备存储。

另一方面,存储的属性确实有后备存储。但getset。相反,它有willSetdidSet,您可以使用它来观察变量,最终触发副作用和/或修改存储的值。对于计算属性,您没有willSetdidSet,并且您不需要它们,因为对于计算属性,您可以使用set中的代码来控制更改。