【问题标题】:What is the purpose of willSet and didSet in Swift?Swift 中 willSet 和 didSet 的作用是什么?
【发布时间】:2014-07-23 06:16:41
【问题描述】:

Swift 的属性声明语法与 C# 非常相似:

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

但是,它也有 willSet 和 didSet 操作。它们分别在调用 setter 之前和之后调用。考虑到您可以在 setter 中使用相同的代码,它们的目的是什么?

【问题讨论】:

  • 我个人不喜欢这里的很多答案。他们在语法上太过分了。差异更多地在于语义和代码可读性。计算属性(get & set)基本上是有一个基于另一个属性的属性computed,例如将标签的text 转换为年份Int。 didSet & willSet 是说...嘿,这个值已经设置好了,现在让我们这样做,例如我们的 dataSource 已更新...所以让我们重新加载 tableView 以便它包含新行。另一个例子见dfri's answer on how to call delegates in didSet
  • 在评论中找到最简单的答案。

标签: swift callback didset property-observer


【解决方案1】:

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

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

myProperty 每次修改时都会打印其旧值和新值。只有 getter 和 setter,我需要这个:

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

所以willSet 和didSet 代表了几行的经济性,并且字段列表中的噪音更少。

【讨论】:

  • 注意:willSet 和 didSet 在您从 init 方法中设置属性时不会被调用,如 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.
  • 但在执行此操作时,它们似乎在数组属性上被调用:myArrayProperty.removeAtIndex(myIndex) ...未预期。
  • 您可以将赋值包装在初始化程序中的 defer { } 语句中,这会导致在退出初始化程序范围时调用 willSet 和 didSet 方法。我不一定推荐它,只是说它是可能的。结果之一是它仅在您声明该属性可选时才有效,因为它不是严格地从初始化程序初始化的。
  • 请在下面解释。我不明白,这个方法还是变量 var propertyChangedListener : (Int, Int) -> Void = { println("The value of myProperty has changed from ($0) to ($1)") }
  • Swift 3 不支持在同一行初始化属性。您应该更改答案以符合 swift 3。
【解决方案2】:

我的理解是 set 和 get 用于 computed properties(没有来自 stored properties 的支持)

如果您来自 Objective-C,请记住命名约定已经改变。在 Swift 中,一个 iVar 或实例变量被命名为 stored property

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

var test : Int {
    get {
        return test
    }
}

这将导致警告,因为这会导致递归函数调用(getter 调用自身)。这种情况下的警告是“尝试在其自己的 getter 中修改 'test'”。

示例 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 Container

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

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

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

【讨论】:

  • 为什么不能使用我将 didSet 与 get 和 set 一起使用..?
  • //I can't see a way to 'stop' the value being set to the same controller - hence the computed property 在我使用 if let newViewController = _childVC { 而不是 if (_childVC) { 后警告消失
  • get 和 set 用于创建计算属性。这些是纯粹的方法,没有后备存储(实例变量)。 willSet 和 didSet 用于观察存储的变量属性的变化。在引擎盖下,这些由存储支持,但在 Swift 中,它们都融合为一个。
  • 在您的示例 5 中,在 get 中,我认为您需要添加 if _childVC == nil { _childVC = something } 然后 return _childVC。
【解决方案3】:

这些被称为 Property Observers:

属性观察者观察并响应属性的变化 价值。每次属性值被调用时,都会调用属性观察者 设置,即使新值与属性的当前值相同 价值。

摘自:Apple Inc. “Swift 编程语言”。电子书。 https://itun.es/ca/jEUH0.l

我怀疑这是为了允许我们传统上使用 KVO 执行的操作,例如与 UI 元素进行数据绑定,或触发更改属性的副作用、触发同步过程、后台处理等。

【讨论】:

    【解决方案4】:

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

    let minValue = 1
    
    var value = 1 {
        didSet {
            if value < minValue {
                value = minValue
            }
        }
    }
    
    value = -10 // value is minValue now.
    

    【讨论】:

      【解决方案5】:

      注意

      willSet 和 didSet 在委托发生之前在初始化程序中设置属性时不会调用观察者

      【讨论】:

        【解决方案6】:

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


        willSet 和 didSet 属性观察器可用于调用委托,例如,对于仅由用户交互更新的类属性,但您希望避免在对象初始化时调用委托。

        我将引用 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(...))。

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

        // 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属性,但是一般来说:在这种情况下,注意不要在关联委托函数的范围内更新customUserControl对象的value属性(这里:@ 987654337@) 在视图控制器中,否则你将得到无限递归。

        【讨论】:

          【解决方案7】:

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

          请注意,willSet 需要一个参数名称来解决,另一方面,didSet 不需要。

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

          【讨论】:

            【解决方案8】:

            Getter 和 setter 有时过于繁重而无法仅仅为了观察适当的值变化而实施。通常这需要额外的临时变量处理和额外的检查,如果你编写了数百个 getter 和 setter,你甚至会想要避免那些微小的劳动。这些东西是针对这种情况的。

            【讨论】:

            • 您是说使用willSet 和didSet 与等效的setter 代码相比有性能 优势吗?这似乎是一个大胆的主张。
            • @zneak 我用错了词。我声称的是程序员的努力,而不是处理成本。
            【解决方案9】:

            在您自己的(基)类中,willSet 和 didSet 非常冗余,因为您可以改为定义访问 @ 的计算属性(即 get- 和 set- 方法) 987654323@ 并进行所需的前期和后期处理。

            如果,然而,你重写了一个属性已经定义的类,那么willSet和didSet是有用而不是多余的!

            【讨论】:

              【解决方案10】:

              didSet 真正方便的一件事是当您使用 outlet 添加额外配置时。

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

              【讨论】:

              • 或使用 willSet 对这个 outlet 方法有一些影响,不是吗?
              【解决方案11】:

              我不懂 C#,但稍微猜测一下,我想我明白了什么

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

              确实如此。它看起来与您在 Swift 中的非常相似,但并不相同:在 Swift 中,您没有 getFoo 和 setFoo。这不是一点点区别:这意味着您没有任何底层存储来满足您的价值。

              Swift 具有存储和计算属性。

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

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

              【讨论】:

              • 这是 Swift 示例。 getFoo 和 setFoo 是简单的占位符,用于您希望 getter 和 setter 执行的任何操作。 C# 也不需要它们。 (在我访问编译器之前,我确实错过了一些语法上的微妙之处。)
              • 哦,好的。但重要的一点是计算属性没有底层存储。另请参阅我的其他答案:stackoverflow.com/a/24052566/574590
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2023-03-26
              • 1970-01-01
              • 2017-02-12
              • 1970-01-01
              • 1970-01-01
              • 2019-12-20
              相关资源
              最近更新 更多