Swift 中lazy 使用方法

lazy 懒加载

今天我们来看看怎样通过变懒la💤y 😴…来提高效率⚡️

问题描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//假设你要做一个聊天应用,想用头像表示你的用户。针对每个头像你都要准备不同分辨率的版本,所以我们这样做:
extension UIImage {
func resizedTo(size: CGSize) -> UIImage {
/* 这里是图片大小调整算法,涉及到大量计算 */
}
}

class Avatar {
static let defaultSmallSize = CGSize(width: 64, height: 64)

var smallImage: UIImage
var largeImage: UIImage

init(largeImage: UIImage) {
self.largeImage = largeImage
self.smallImage = largeImage.resizedTo(Avatar.defaultSmallSize)
}
}

以上代码的弊端:我们需要在 init 中计算出 smallImage 的,因为编译器要求我们在 init 中初始化 Avatar 的所有属性。

但我们有可能会使用另一个小尺寸版本的用户头像,那么这个缺省值(smallImage)根本就不会被用到。也就是说,我们用计算量很大的图像缩放算法算出了这个缺省值,却没有任何卵用。

一种可能的解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//在 Objective-C 里,类似情况我们往往会使用一个中间私有变量(intermediate private variable),该用法翻译成 Swift 是这样:
class Avatar {
static let defaultSmallSize = CGSize(width: 64, height: 64)

private var _smallImage: UIImage?
var smallImage: UIImage {
get {
if _smallImage == nil {
_smallImage = largeImage.resizedTo(Avatar.defaultSmallSize)
}
return _smallImage! // 🐴
}
set {
_smallImage = newValue
}
}
var largeImage: UIImage

init(largeImage: UIImage) {
self.largeImage = largeImage
}
}

用这种方式,我们就可以随时给 smallImage 赋一个新值。但是如果在使用它之前没有赋过值,它不会返回 nil,而是基于 largeImage 计算一个值并返回。

这恰好满足我们的需求。但是代码实在太多。想想看,如果每个头像需要准备更多分辨率的版本,而且对每种版本都有这种需求,那该多恐怖!

Swift的惰性初始化(lazy initialization)

多亏了 Swift,我们可以省掉上面那些胶水代码(glue code),偷点儿懒… 只要简单的把 smallImage 变量声明成一个 lazy 存储属性即可!

1
2
3
4
5
6
7
8
9
10
class Avatar {
static let defaultSmallSize = CGSize(width: 64, height: 64)

lazy var smallImage: UIImage = self.largeImage.resizedTo(Avatar.defaultSmallSize)
var largeImage: UIImage

init(largeImage: UIImage) {
self.largeImage = largeImage
}
}

搞定了,使用 lazy 关键字,我们用更少代码实现了相同的行为!

如果我们在给 smallImage 惰性变量赋一个特定值之前使用了它,那么当且仅当此时该变量的缺省值才会被计算并返回。如果随后我们再次使用这个属性,它的值就已经被计算出来,会直接返回这个已存储的值。

如果我们在访问 smallImage 之前给它赋一个确切的值,那它就不会浪费时间计算那个缺省值,(随后访问它时)返回的是我们之前给它的那个确切的值。

如果我们永远不访问 smallImage 这个属性,那它的缺省值就永远不会被计算出来!

这就是一种可以避免无用初始化的有效且简单的方法,而且在没有使用中间私有变量的情况下提供缺省值。

用一个闭包做初始化

和其他属性一样,你可以用一个原地计算(in-place-evaluated)闭包来给 lazy 变量设定缺省值 - 使用= { /* some code */ }()替换掉= some code。当你需要多行代码去计算缺省值时,这么做更好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Avatar {
static let defaultSmallSize = CGSize(width: 64, height: 64)

lazy var smallImage: UIImage = {
let size = CGSize(
width: min(Avatar.defaultSmallSize.width, self.largeImage.size.width),
height: min(Avatar.defaultSmallSize.height, self.largeImage.size.height)
)
return self.largeImage.resizedTo(size)
}()
var largeImage: UIImage

init(largeImage: UIImage) {
self.largeImage = largeImage
}
}

但由于它是一个 lazy 属性,所以你可以在闭包里引用self!(注意,即使不用闭包你也可以引用 self,之前的例子就是。)

属性是 lazy 意味着它的缺省值暂时不会计算,当它需要计算的时候,self 已经完成初始化。这就是为什么你可以在那里使用 self ——这和非 lazy 属性正好相反:它的缺省值在初始化阶段就被计算出来了。

ℹ️瞬发闭包(Immdiately-applied closures),比如上面给 lazy 变量做缺省值的那个闭包,它是自动@noescape的。这就意味着在这个闭包中无需加[unowned self]:这里不会产生引用循环。

lazy let?

在 Swift 里你不能创建 lazy let 实例属性,因此无法实现一个使用时才会被计算的常量😢。这是由 lazy 的具体实现细节决定的:它在没有值的情况下以某种方式被初始化,然后在被访问时改变自己的值,这就要求该属性是可变的1。

既然我们说到了 let,顺便说一条比较有意思的特性:被声明在全局作用域下、或者被声明为一个类型属性(声明为static let、而非声明为实例属性)的常量是自动具有惰性(lazy)的(还是线程安全的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 全局变量,被以 lazy 形式(和一种线程安全的形式)创建
let foo: Int = {
print("Global constant initialized")
return 42
}()

class Cat {
static let defaultName: String = {
print("Type constant initialized")
return "Felix"
}()
}

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
print("Hello")
print(foo)
print(Cat.defaultName)
print("Bye")
return true
}
}

这段代码会先打印出 Hello,然后是 Global constant initialized 和 42,接下来是 Type constant initialized 和 Felix,最后是 Bye;证明了 foo 和 Cat.defaultName 这两个常量只在被访问时才被创建,而非初始化时创建。

别把这个和class或结构体里面的实例属性的情况搞混了。如果你声明一个struct Foo { let bar = Bar() },那 bar 这个实例属性会在一个 Foo 实例被创建的时候就被计算出来(作为其初始化的一部分),而不是以惰性的形式。

另一个例子:Sequences

我们再举一个例子,这次是 sequence(序列)/ Array,以及一些高阶函数,比如 map:

1
2
3
4
5
6
7
8
9
func increment(x: Int) -> Int {
print("Computing next value of \(x)")
return x+1
}

let array = Array(0..<1000)
let incArray = array.map(increment)
print("Result:")
print(incArray[0], incArray[4])

对这段代码来说,在我们访问 incArray 的值之前,所有的输出值都被计算出来了。所以在 print(“Result:”)被执行之前你会看到有 1000 行 Computing next value of …!即使我们只读了[0]和[4]这两个条目,根本就没关心其他剩下的… 想想假如我们用的函数计算量比 increment 更大会怎样!

Lazy sequences(惰性序列)

OK,我们来用另一种形式的 lazy 解决上面的问题。

在 Swift 标准库中,SequenceType 和 CollectionType 协议都有个叫 lazy 的计算属性,它能给我们返回一个特殊的 LazySequence 或者 LazyCollection。这些类型只能被用在 map,flatMap,filter这样的高阶函数中,而且是以一种惰性的方式。

来看看如何使用

1
2
3
4
let array = Array(0..<1000)
let incArray = array.lazy.map(increment)
print("Result:")
print(incArray[0], incArray[4])

现在这段代码只打印出来这些

1
2
3
4
Result:
Computing next value of 0…
Computing next value of 4…
1 5

证明了只是在那些值被使用时才调用 increment 函数,而不是调用 map 的时候。并且只对那些被访问到的值使用,而不是对整个数组里面一千个值都使用!🎉

这下效率提高了很多!对那些涉及到庞大的序列(比如这个有 1000 个元素的数组)、以及高计算度闭包的情景来说,使用这个技巧会带来质变。

将惰性序列级联

有关惰性序列,最后一个小妙招就是你可以像 monad 那样,把高阶函数的调用拼接起来。比如你可以让一个惰性序列以这种方式调用 map(或者 flatMap):

1
2
3
4
5
6
func double(x: Int) -> Int {
print("Computing double value of \(x)…")
return 2*x
}
let doubleArray = array.lazy.map(increment).map(double)
print(doubleArray[3])

这样只有当 array[3] 被访问时,double(increment(array[3])) 才会被执行,被访问之前不会有这个计算,数组的其他元素也不会有这个计算!

与之相对,如果使用 array.map(increment).map(double)[3](不带 lazy)会首先对整个 array 序列的所有元素进行计算,所有结果都计算出来之后再提取出第4个元素。更糟糕的是对数组的迭代要进行两次,每个 map 都会有一次。这对计算时间(computational time)来说是怎样的一种浪费!

结论

懒惰是人类文明进步的真正动力

在 class 中使用 static let 是 Swift 创建单例的最佳实践(即使你应该避免使用单例😉),原因在于 static let 是惰性的、线程安全的,而且只能被创建一次。

“高阶函数”是一种能把另外一个函数当做参数或者能返回一个函数(或者两者都能)的函数。常见的高阶函数有 map,flatMap,filter等等。

文章引用自SwiftGG