【问题标题】:Is it normal that lazy var property is initialized twice?惰性 var 属性被初始化两次是否正常?
【发布时间】:2015-08-06 04:17:27
【问题描述】:

当我使用带有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"
    }
}

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

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 是不正确的。

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

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

【问题讨论】:

  • “我也有猜测,初始化代码中给self.view添加了什么testLabel是不正确的”是不正确的。它会造成可怕的副作用,因为您创建view 太快了。不要将主要的副作用添加到单纯的初始化程序中!只需制作标签并将其退回。将其添加为viewDidLoad 中的子视图。
  • 我已经准备好提供更多信息,但我仍然不知道是否将其称为 Swift 的错误或您的错误。让我将其添加到我的答案中。
  • 好的,一切就绪。我相信我已经完全解释了这个问题。我还没有决定是否称它为 Swift 错误。这当然是一个边缘案例,但它是一个你一开始就不应该接近的边缘。 :) 尽管如此,您还是有理由将测试项目作为错误报告提交给 Apple;他们可能想知道这件事。
  • 太棒了!你的解释是最合理的。我无法捕捉到放置在调用堆栈中的点。这显然是递归产生了意想不到的流动。我不确定 Swift lazy 是否必须反对这个流程。无论如何,我会将其发布到Apple。谢谢。
  • 简短的回答是我们应该从不在惰性块中访问view。改为在viewDidLoad 中进行

标签: ios swift lazy-initialization


【解决方案1】:

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

  • 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 的错误,但如果是这样,那是一个在现实生活中根本不应该遇到的错误。


编辑:

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

【讨论】:

  • 我觉得你的回答很好!实际上,我也猜到你指出了什么。谢谢你。但是,我仍然不清楚为什么延迟初始化会运行两次。 @马特
  • 是的,我只是尝试应用您的建议,问题就解决了。初始化不再运行两次。
  • 感谢您提供更多信息。你能告诉我你看过的视频的标题吗?
猜你喜欢
  • 1970-01-01
  • 2019-05-03
  • 2021-11-21
  • 2015-11-29
  • 2012-11-25
  • 1970-01-01
  • 2020-10-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多