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

时间:2015-05-24 16:04:08

标签: ios swift lazy-initialization

我遇到过使用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>

如您所见,初始化代码执行两次。我不知道这是一个错误或误用。有没有人让我知道出了什么问题?

我还猜测它并不能纠正初始化代码中testLabel添加self.view的内容。我不确定代码是不正确的。这只是我的猜测。

更新
我仍然不明白懒惰初始化运行两次的原因。这真的是斯威夫特的错误吗?

最终更新:
@matt对这个问题进行了两次初始化做了很好的解释。虽然所有内容都来自我的错误代码,但我可以获得有关lazy关键字如何工作的宝贵知识。谢谢亚光。

1 个答案:

答案 0 :(得分:23)

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

  • prepareForSegue中,您不得引用目标视图控制器的接口,因为它没有接口viewDidLoad还没有运行;视图控制器没有视图,没有插座,没有任何东西。

  • 您对label属性的延迟初始化程序也不应该将标签添加到接口。它应该制作标签并将其退回。

其他要知道的事情:

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

  • 询问视图控制器其视图是否已加载而不强制视图过早加载的唯一方法是使用isViewLoaded()

您想要做的正确程序是:

  • prepareForSegue中,将名称字符串分配给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和并发的WWDC 2016视频中,Apple明确 。在Swift 1和2中,甚至在Swift 3中,lazy实例变量不是原子,因此如果同时从两个上下文调用,初始化程序可以运行两次 - 这正是你的代码所做的。