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

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

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

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

236321 次浏览

重点似乎是,有时,你需要一个具有自动存储而且某些行为的属性,例如通知其他对象该属性刚刚更改。当你只有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
}
}
}

因此willSetdidSet表示节省了几行,并且字段列表中的噪音更少。

它们被称为财产观察员:

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

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

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

Getter和setter有时太笨重,不能仅仅为了观察正确的值变化而实现。通常这需要额外的临时变量处理和额外的检查,如果你写了数百个getter和setter,你会想要避免这些微小的劳动。这些东西是为这种情况准备的。

我不懂c#,但稍微猜一下,我想我懂了

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

所做的事。它看起来非常类似于你在Swift中拥有的,但它是不一样的:在Swift中你没有getFoosetFoo。这不是一个小的区别:这意味着你没有任何底层存储你的价值。

Swift已经存储和计算了属性。

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

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

我的理解是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。读取/写入计算属性-带有备份存储

下面是一个允许对实际存储属性进行条件设置的模式

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

在你自己的(基)类中,willSetdidSet完全是reduntant,因为你可以定义一个计算属性(即get-和set-方法)来访问_propertyVariable并执行期望的前后处理

如果然而重写属性为已经定义然后的类,则willSetdidSet有用而不多余!

请注意

当在委托发生之前在初始化式中设置属性时,不会调用willSetdidSet观察器

也可以使用didSet将变量设置为不同的值。这不会像性能指南中所述的那样再次调用观察者。例如,当你想限制值如下所示时,它是有用的:

let minValue = 1


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


value = -10 // value is minValue now.

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


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

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

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(...))一样。

对于这个简单的例子,使用来自类属性valuedidSet的委托回调来告诉视图控制器它的一个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属性,否则你将以无限递归结束。

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

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