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

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

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


当前回答

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


willSet和didSet属性观察者可以用来调用委托,例如,对于只由用户交互更新的类属性,但是你想避免在对象初始化时调用委托。

我将引用克拉斯对已接受答案的向上投票评论:

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

这是非常简洁的,因为这意味着例如,didSet属性是委托回调和函数的启动点,对于你自己的自定义类来说是一个很好的选择。

例如,考虑一些自定义用户控件对象,带有一些关键属性值(例如,在评级控件中的位置),实现为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的委托回调来告诉视图控制器它的一个outlet已经有了相关的模型更新:

// 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属性已经被封装,但通常:在这样的情况下,注意不要在视图控制器中关联的委托函数(此处:didChangeValue())的范围内更新customUserControl对象的value属性,否则你将以无限递归结束。

其他回答

重点似乎是,有时您需要一个具有自动存储和某些行为的属性,例如通知其他对象该属性刚刚更改。当您只有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代表了两行代码的节省,并且字段列表中的噪音更少。

这些被称为属性观察员:

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

摘自:苹果公司《快速编程语言》。“iBooks。https://itun.es/ca/jEUH0.l

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

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


willSet和didSet属性观察者可以用来调用委托,例如,对于只由用户交互更新的类属性,但是你想避免在对象初始化时调用委托。

我将引用克拉斯对已接受答案的向上投票评论:

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

这是非常简洁的,因为这意味着例如,didSet属性是委托回调和函数的启动点,对于你自己的自定义类来说是一个很好的选择。

例如,考虑一些自定义用户控件对象,带有一些关键属性值(例如,在评级控件中的位置),实现为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的委托回调来告诉视图控制器它的一个outlet已经有了相关的模型更新:

// 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属性已经被封装,但通常:在这样的情况下,注意不要在视图控制器中关联的委托函数(此处:didChangeValue())的范围内更新customUserControl对象的value属性,否则你将以无限递归结束。

我的理解是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
        }
    }
}

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

例3。读取/写入计算属性-带有备份存储

Here is a pattern that allows conditional setting of an actual stored property
//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来转发通知到viewController(参见UIViewController文档和viewController容器的信息)

如果我在任何地方犯了错误,请编辑修改!

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

注意,willSet需要一个参数名来处理,另一方面,didSet不需要。

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