290

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

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

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

4

11 回答 11

352

重点似乎是有时,您需要一个具有自动存储某些行为的属性,例如通知其他对象该属性刚刚更改。当您只有get/set时,您需要另一个字段来保存该值。使用willSetdidSet,您可以在修改值时采取行动,而无需其他字段。例如,在该示例中:

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代表了几行的经济性,并且字段列表中的噪音更少。

于 2014-06-03T02:38:50.800 回答
159

我的理解是 set 和 get 用于计算属性(没有存储属性的支持)

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

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

类似的问题 -你不能这样做,因为它递归地调用设置器。另外,请注意,此代码不会抱怨没有初始化程序,因为没有要初始化的存储属性

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

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

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

于 2014-06-26T11:51:21.663 回答
21

这些被称为属性观察者

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

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

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

于 2014-06-03T02:53:14.577 回答
21

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

let minValue = 1

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

value = -10 // value is minValue now.
于 2015-10-05T11:48:58.197 回答
20

笔记

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

于 2015-08-18T10:09:49.170 回答
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.
}

之后,您的委托函数可以在某些视图控制器中使用,以观察 for 模型中的关键变化CustomViewController,就像您使用UITextFieldDelegateforUITextField对象(例如textFieldDidEndEditing(...))的固有委托函数一样。

对于这个简单的例子,使用didSet类属性的委托回调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属性,否则您最终会得到无限递归。customUserControldidChangeValue()

于 2015-12-08T12:43:17.113 回答
5

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

请注意,willSet另一方面,需要一个参数名称才能解决didSet

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

于 2015-06-13T08:08:05.973 回答
2

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

于 2014-06-03T03:26:31.640 回答
2

在您自己的(基)类中,willSet并且didSet非常冗余,因为您可以改为定义一个计算属性(即 get- 和 set- 方法)来访问 a_propertyVariable并执行所需的预处理和后处理

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

于 2015-07-23T22:55:57.440 回答
1

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

@IBOutlet weak var loginOrSignupButton: UIButton! {
  didSet {
        let title = NSLocalizedString("signup_required_button")
        loginOrSignupButton.setTitle(title, for: .normal)
        loginOrSignupButton.setTitle(title, for: .highlighted)
  }
于 2017-06-22T14:19:14.207 回答
-5

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

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

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

Swift 具有存储和计算属性。

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

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

于 2014-06-05T08:08:42.917 回答