lazy var 属性 初始化两次正常吗?

Is it normal that lazy var property is initialized twice?

有一次我用 属性 和 lazy 关键字时遇到了很奇怪的情况。我知道这个关键字表示 属性 的初始化将被推迟到实际使用该变量时。但是,它没有像我预期的那样工作。它 运行 两次。

class TestLazyViewController: UIViewController {

    var name: String = "" {
        didSet {
            NSLog("name self = \(self)")
            testLabel.text = name
        }
    }

    lazy var testLabel: UILabel = {
        NSLog("testLabel self = \(self)")
        let label = UILabel()
        label.text = "hello"
        self.view.addSubview(label)
        return label
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        testLabel.setTranslatesAutoresizingMaskIntoConstraints(false)
        NSLayoutConstraint.activateConstraints([NSLayoutConstraint(item: testLabel, attribute: .CenterX, relatedBy: .Equal, toItem: self.view, attribute: .CenterX, multiplier: 1.0, constant: 0.0)])
        NSLayoutConstraint.activateConstraints([NSLayoutConstraint(item: testLabel, attribute: .CenterY, relatedBy: .Equal, toItem: self.view, attribute: .CenterY, multiplier: 1.0, constant: 0.0)])
    }

    @IBAction func testButton(sender: AnyObject) {
        testLabel.text = "world"
    }
}

我写了一个视图控制器来测试。该视图控制器由另一个视图控制器呈现。然后,name 属性 设置在呈现视图控制器的 prepareForSegue 中。

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    let vc = segue.destinationViewController as! TestLazyViewController
    println("vc = \(vc)")
    vc.name = "hello"
}

在 运行 测试中,我得到了以下结果。

vc = <testLazy.TestLazyViewController: 0x7fb3d1d16ec0>
2015-05-25 00:26:15.673 testLazy[95577:22267122] name self = <testLazy.TestLazyViewController: 0x7fb3d1d16ec0>
2015-05-25 00:26:15.673 testLazy[95577:22267122] testLabel self = <testLazy.TestLazyViewController: 0x7fb3d1d16ec0>
2015-05-25 00:26:15.674 testLazy[95577:22267122] testLabel self = <testLazy.TestLazyViewController: 0x7fb3d1d16ec0>

如您所见,初始化代码执行了两次。我不知道这是一个错误还是滥用的结果。有没有人可以让我知道哪里出了问题?

我猜测在初始化代码中用 self.view 引用 testLabel 是不正确的。

更新:
我还是不明白为什么要延迟初始化运行s 两次。真的是Swift的bug吗?

最终更新:
@matt 对这个被初始化两次的问题做了很好的解释。我能够获得有关 lazy 关键字如何工作的宝贵知识。谢谢马特。

您的代码的整个概念都是错误的。

  • prepareForSegue中,你不能引用目标视图控制器的接口,因为它没有接口viewDidLoad 还没有 运行;视图控制器没有视图,没有插座,什么都没有。

  • 标签 属性 的惰性初始值设定项不应该也将标签添加到接口。它应该只制作标签和 return 它。

其他需要知道的事情:

  • 在视图控制器具有视图之前引用视图控制器的 view 将强制该视图过早加载。这样做错误实际上会导致视图加载两次,这可能会产生可怕的后果。

  • 在不强制视图过早加载的情况下询问视图控制器其视图是否已加载的唯一方法是 isViewLoaded().

您想要执行的操作的正确步骤是:

  • prepareForSegue中,将name字符串赋给一个name属性就可以了。它可以有一个观察者,但是如果我们当时没有view,那个观察者一定不能引用view,因为这样做会导致view过早加载。

  • viewDidLoad 中,只有那时我们才有视图,现在您可以开始填充界面了。 viewDidLoad 应创建标签,将其放入界面,然后选择 name 属性,并将其分配给标签。


编辑:

现在,说了那么多...这与您原来的问题有什么关系?你在这里做错了什么解释了 Swift 正在做的事情,以及 Swift 自己做错了什么?

要查看答案,只需在以下位置设置断点:

lazy var testLabel: UILabel = {
    NSLog("testLabel self = \(self)") // breakpoint here
    // ...

您会看到,由于您构造代码的方式,我们递归 两次获得 testLabel 的值。这是调用堆栈,略有简化:

prepareForSegue
name.didset
testLabel.getter -> *
viewDidLoad
testLabel.getter -> *

testLabel getter 指的是视图控制器的 view,这导致视图控制器的视图被加载,因此它的 viewDidLoad 被调用并导致testLabel getter 待重拨。

请注意 getter 不仅仅是按顺序调用两次。它被递归调用两次:它本身实际上是在调用自己。

Swift 未能防御的正是这种递归。如果 setter 仅仅被连续调用两次,惰性初始化器就不会被调用第二次。但在你的情况下,它是递归的。所以确实是第二次,惰性初始化器之前从来没有运行。它已经开始,但从未完成。因此,Swift 现在 运行 宁它是合理的 - 这恰好意味着 运行 再次宁它。

所以,从某种意义上说,是的,你已经抓住了 Swift 的裤子,但是为了实现这一点你必须做的事情是如此离谱以至于它可以被合理地称为你自己的过错。这可能是 Swift 的错误,但如果是这样,那是一个在现实生活中根本不应该遇到的错误。


编辑:

在关于 Swift 和并发的 WWDC 2016 视频中,Apple 明确 对此。在 Swift 1 和 2 中,甚至在 Swift 3 中,lazy 实例变量 不是原子的 ,因此初始化程序可以 运行 twice 如果同时从两个上下文调用 — 这正是您的代码所做的。