是否可以允许在 Swift 初始化期间调用 didSet?

Is it possible to allow didSet to be called during initialization in Swift?

提问人:Logan 提问时间:8/11/2014 最后编辑:Slipp D. ThompsonLogan 更新时间:7/7/2020 访问量:66294

问:

问题

Apple 的文档指定:

首次初始化属性时,不会调用 willSet 和 didSet 观察程序。仅当属性的值在初始化上下文之外设置时,才会调用它们。

是否可以在初始化期间强制调用这些?

为什么?

假设我有这个类

class SomeClass {
    var someProperty: AnyObject {
        didSet {
            doStuff()
        }
    }

    init(someProperty: AnyObject) {
        self.someProperty = someProperty
        doStuff()
    }

    func doStuff() {
        // do stuff now that someProperty is set
    }
}

我创建了 方法 ,以使处理调用更简洁,但我宁愿只处理函数中的属性。有没有办法在初始化期间强制调用它?doStuffdidSet

更新

我决定只删除我的类的便利初始化程序,并强制您在初始化后设置属性。这让我知道将永远被召唤。我还没有决定这是否总体上更好,但它很适合我的情况。didSet

iOS Swift Didset

评论

1赞 Fattie 10/30/2017
老实说,最好是“接受这就是 Swift 的工作方式”。如果你在 var 语句上放置一个内联初始化值,当然不会调用“didSet”。这就是为什么“init()”情况也不能调用“didSet”的原因。这一切都是有道理的。
0赞 AamirR 6/28/2018
@Logan 这个问题本身回答了我的问题;)谢谢!
2赞 Darek Cieśla 10/25/2018
如果你想使用便利初始值设定器,你可以把它与:deferconvenience init(someProperty: AnyObject) { self.init() defer { self.someProperty = someProperty }

答:

125赞 Oliver 8/11/2014 #1

创建一个自己的 set-Method 并在 init-Method 中使用它:

class SomeClass {
    var someProperty: AnyObject! {
        didSet {
            //do some Stuff
        }
    }

    init(someProperty: AnyObject) {
        setSomeProperty(someProperty)
    }

    func setSomeProperty(newValue:AnyObject) {
        self.someProperty = newValue
    }
}

通过声明为 type: (隐式 unwrapped 可选),您允许 self 在不设置的情况下完全初始化。当您调用时,您调用的等效于 .通常你无法做到 这样做是因为 self 尚未完全初始化。由于不需要初始化,并且您正在调用 方法依赖于 self,Swift 离开初始化上下文和 didSet 将运行。somePropertyAnyObject!somePropertysetSomeProperty(someProperty)self.setSomeProperty(someProperty)someProperty

评论

3赞 mmmmmm 8/11/2014
为什么这与 init 中的 self.someProperty = newValue 不同?你有可用的测试用例吗?
2赞 Oliver 8/11/2014
不知道它为什么有效 - 但它确实有效。直接在 init()-Method 中设置新值不会调用 didSet() -- 但使用给定的方法 setSomeProperty() 会调用。
54赞 Logan 8/11/2014
我想我知道这里发生了什么。通过声明为 type:(隐式未包装的可选),您可以允许在不设置的情况下完全初始化。当您调用时,您调用的等效于 .通常,您将无法执行此操作,因为尚未完全初始化。由于不需要初始化,并且您正在调用依赖于 的方法,因此 Swift 会离开初始化上下文并运行。somePropertyAnyObject!selfsomePropertysetSomeProperty(someProperty)self.setSomeProperty(someProperty)selfsomePropertyselfdidSet
3赞 WCByrne 3/14/2015
看起来在实际 init() 之外设置的任何内容都会被调用 didSet。从字面上看,任何未设置的内容都将调用 didSet。非常适合这个问题,但使用辅助函数设置某些值会导致调用 didSet。如果您想重用该 setter 函数,那就不是很好了。init() { *HERE* }
4赞 Dan Rosenstark 4/8/2016
@Mark说真的,试图将常识或逻辑应用于 Swift 是荒谬的。但您可以相信赞成票或自己尝试。我只是做了,它有效。是的,这根本没有意义。
85赞 original_username 4/8/2015 #2

作为 Oliver 答案的变体,您可以将线条包裹在闭合中。例如:

class Classy {

    var foo: Int! { didSet { doStuff() } }

    init( foo: Int ) {
        // closure invokes didSet
        ({ self.foo = foo })()
    }

}

编辑:布莱恩·韦斯特法尔(Brian Westphal)的回答更好,恕我直言。他的好处是它暗示了意图。

评论

2赞 pointum 9/4/2015
这很聪明,不会用冗余的方法污染类。
0赞 Max 3/25/2016
很好的答案,谢谢!值得注意的是,在 Swift 2.2 中,您需要始终将闭包括在括号中。
0赞 original_username 3/29/2016
@Max 干杯,样本现在包括 parens
3赞 kcstricks 4/7/2016
聪明的答案!需要注意的是:如果你的类是其他类的子类,你需要在 ({ self.foo = foo })() 之前调用 super.init()
1赞 original_username 4/29/2016
@Hlung,我不觉得它在未来比其他任何东西都更有可能崩溃,但仍然存在一个问题,即如果不注释代码,就不太清楚它的作用。至少 Brian 的“延迟”回答给了读者一个提示,即我们希望在初始化后执行代码。
-1赞 yshilov 9/3/2015 #3

你可以用 obj-с 的方式解决它:

class SomeClass {
    private var _someProperty: AnyObject!
    var someProperty: AnyObject{
        get{
            return _someProperty
        }
        set{
            _someProperty = newValue
            doStuff()
        }
    }
    init(someProperty: AnyObject) {
        self.someProperty = someProperty
        doStuff()
    }

    func doStuff() {
        // do stuff now that someProperty is set
    }
}

评论

1赞 Logan 9/3/2015
嗨,@yshilov,我认为这并没有真正解决我希望的问题,因为从技术上讲,当您调用时,您将离开初始化范围。我相信同样的事情也可以通过做来完成。不过,有些人可能会将其视为一种解决方法,感谢您的添加self.somePropertyvar someProperty: AnyObject! = nil { didSet { ... } }
0赞 yshilov 9/3/2015
@Logan不,那行不通。didSet 将在 init() 中被完全忽略。
384赞 Brian Westphal 11/29/2015 #4

如果在初始值设定项内部使用,用于更新任何可选属性或进一步更新已初始化的非可选属性,并且在调用任何方法后,将调用 、 等。我发现这比实现单独的方法更方便,您必须在正确的地方跟踪调用。defersuper.init()willSetdidSet

例如:

public class MyNewType: NSObject {

    public var myRequiredField:Int

    public var myOptionalField:Float? {
        willSet {
            if let newValue = newValue {
                print("I'm going to change to \(newValue)")
            }
        }
        didSet {
            if let myOptionalField = self.myOptionalField {
                print("Now I'm \(myOptionalField)")
            }
        }
    }

    override public init() {
        self.myRequiredField = 1

        super.init()

        // Non-defered
        self.myOptionalField = 6.28

        // Defered
        defer {
            self.myOptionalField = 3.14
        }
    }
}

将产生:

I'm going to change to 3.14
Now I'm 3.14

评论

13赞 Chris Hatton 11/15/2016
厚颜无耻的小把戏,我喜欢......不过,斯威夫特的怪癖。
7赞 Ryan 11/23/2016
伟大。您发现了另一个有用的使用 的案例。谢谢。defer
2赞 Christian Schnorr 6/15/2017
很好地利用了延迟!希望我能给你不止一个赞成票。
4赞 aBikis 6/28/2017
很有意思。.我以前从未见过像这样使用 defer。它雄辩且有效。
0赞 Yakiv Kovalskyi 7/13/2018
但是您设置了两次 myOptionalField,从性能角度来看,这并不好。如果进行大量计算怎么办?
1赞 Snowman 12/12/2015 #5

虽然这不是一个解决方案,但另一种方法是使用类构造函数:

class SomeClass {
    var someProperty: AnyObject {
        didSet {
            // do stuff
        }
    }

    class func createInstance(someProperty: AnyObject) -> SomeClass {
        let instance = SomeClass() 
        instance.someProperty = someProperty
        return instance
    }  
}

评论

1赞 Mick MacCallum 1/31/2018
此解决方案将要求您更改 的可选性,因为它在初始化期间不会为其提供值。someProperty
2赞 onmyway133 5/12/2016 #6

如果您在子类中执行此操作,则此操作有效

class Base {

  var someProperty: AnyObject {
    didSet {
      doStuff()
    }
  }

  required init() {
    someProperty = "hello"
  }

  func doStuff() {
    print(someProperty)
  }
}

class SomeClass: Base {

  required init() {
    super.init()

    someProperty = "hello"
  }
}

let a = Base()
let b = SomeClass()

例如,未触发。但例如,被触发,因为它在子类中。它必须与真正意味着什么做一些事情,在这种情况下,确实关心这一点adidSetbdidSetinitialization contextsuperclass

评论

0赞 Roshan Sah 10/14/2020
嗨@onmyway133 swift 是一种类型安全的语言,总是完全初始化一个类型,否则不会。初始化 someClass 时。基类正在完全初始化,然后在 SomeClass 中设置 someProperty = “hello”。因此,didSet 被调用
1赞 Cœur 3/23/2017 #7

在特定情况下,如果要调用超类中可用的属性或内部,则可以直接分配超级属性:willSetdidSetinit

override init(frame: CGRect) {
    super.init(frame: frame)
    // this will call `willSet` and `didSet`
    someProperty = super.someProperty
}

请注意,在这种情况下,带有闭包的查尔斯主义解决方案也始终有效。所以我的解决方案只是一个替代方案。

11赞 Carmine Cuofano 7/26/2017 #8

我遇到了同样的问题,这对我有用

class SomeClass {
    var someProperty: AnyObject {
        didSet {
            doStuff()
        }
    }

    init(someProperty: AnyObject) {
        defer { self.someProperty = someProperty }
    }

    func doStuff() {
        // do stuff now that someProperty is set
    }
}

评论

0赞 Vanya 10/28/2017
你从哪里得到的?
4赞 Edward B 5/11/2018
这种方法不再有效。编译器抛出两个错误: – 在所有存储属性初始化之前,在方法调用“$defer”中使用了“self” – 从初始值设定项返回而不初始化所有存储属性
1赞 Mark 4/10/2019
你是对的,但有一个解决方法。您只需要将 someProperty 声明为隐式、未包装或可选,并为其提供默认值和 nil 合并以避免失败。顶级域名;someProperty 必须是可选类型
1赞 Xaxxus 7/7/2020 #9

遗憾的是,初始化根类时不会调用 didSet 观察器。

如果你的类不是子类,则必须使用 getter 和 setter 来实现所需的功能:

class SomeClass {
    private var _test: Int = 0
    var test: Int {
        get { _test }
        set { _test = newValue }
    }
    
    init(test: Int) { self.test = test }
}

或者,如果你的类是一个子类,你可以使用 didSet 并执行以下操作:

override init(test: int) {
    super.init()
    self.test = test
}

didSet 应该在调用 super.init() 之后调用。

有一件事我没有尝试过,但也可能有效:

init(test: int) {
    defer { self.test = test }
}

注意:您需要使属性为 null,或为它们设置默认值,或解开类属性的包装。