术语延迟并非贬义;它是对一种重要行为的正式描述。如果存储变量在声明时被赋予一个初始值,并且使用了延迟初始化,那么直到运行着的代码访问了该变量的值时才会计算初始值并完成赋值。
在Swift中,有3种类型的变量可以做到延迟初始化:
全局变量
全局变量自动就是延迟初始化的,如果你问自己,它们何时应该初始化,那么这就是答案。当应用启动时,文件与顶层代码都会执行,这时初始化全局变量是没有意义的,因为应用甚至还没有运行。这样,全局初始化必须要延迟到后面某个有意义的时间点处。因此,全局变量初始化直到其他代码首次引用它们时才会发生。在底层,该行为是由dispatch_once保护的;这使得初始化只会执行一次并且是线程安全的。
静态属性
静态属性的行为非常类似于全局变量,并且也是出于相同的原因。(Swift中并没有存储类属性,因此类属性是无法初始化的,也不能做到延迟初始化。)
实例属性
在默认情况下,实例属性不是延迟初始化的,不过可以在声明中通过关键字lazy让它变成延迟初始化。该属性必须要通过var声明,而不是let。如果在代码获取属性值之前有其他代码对该属性赋值,那么属性的初始化器就永远都不会执行。
延迟初始化器通常用于实现单例。单例是一种设计模式,所有代码都可以访问某个类的一个单独的共享实例:
class MyClass { static let sharedMyClassSingleton = MyClass}
其他代码可以通过MyClass.sharedMyClassSingleton获取到对MyClass单例的引用。直到其他代码首次这么调用时,单例实例才会创建出来;随后,无论调用多少次,返回的总是这个相同的实例。(如果这是计算只读属性,其getter调用了MyClass()并返回该实例,那么情况就不是这样的了,知道原因吗?)
现在来谈谈实例属性的延迟初始化。为何需要这个特性呢?一个原因是显而易见的:初始值的生成代价可能会很高,因此你希望只在需要时才生成。不过还有另外一个不那么明显的原因,而且这个原因更为重要:延迟初始化器可以做到正常的初始化器做不到的事情。特别地,它可以引用到实例,正常的初始化器却做不到这一点,因为在正常的初始化器运行时,实例还不存在(我们还在创建实例的过程中,因此实例尚未准备好)。与之相反,延迟初始化器直到实例已经创建出来后的某个时间点才会运行,因此可以引用到实例。比如,如果没有将arrow属性声明为lazy,那么如下代码就是不合法的:
class MyView : UIView { lazy var arrow : UIImage = self.arrowImage func arrowImage -> UIImage { // ... big image-generating code goes here ... }}
常见的写法是通过一个定义与调用匿名函数来初始化延迟实例属性:
lazy var prog : UIProgressView = { let p = UIProgressView(progressViewStyle: .Default) p.alpha = 0.7 p.trackTintColor = UIColor.clearColor p.progressTintColor = UIColor.blackColor p.frame = CGRectMake(0, 0, self.view.bounds.size.width, 20) p.progress = 1.0 return p}
语言中有一些小陷阱:延迟实例属性不能拥有Setter观察者,并且也没有lazy let(因此无法将延迟实例属性设为只读)。不过,这些限制并不严重,因为对于存储属性来说,计算属性做不到的事情也不要指望lazy属性能够做到,如示例3-1所示。
示例3-1:手工实现延迟属性
private var lazyOncer : dispatch_once_t = 0private var lazyBacker : Int = 0var lazyFront : Int { get { dispatch_once(&self.lazyOncer) { self.lazyBacker = 42 // expensive initial value } return self.lazyBacker } set { dispatch_once(&self.lazyOncer) {} // will set self.lazyBacker = newValue // did set }}
在示例3-1中,原则在于只有lazyFront可以被外界访问;lazyBacker是其底层存储,lazyOncer使得一切只出现正确的次数。lazyFront现在是个普通的计算变量,因此我们可以在设置时观察它(在其Setter函数中加入额外代码,位于“will set”与“did setter”注释处),也可以将其设为只读(将整个Setter删除)。