Swift 中lazy 使用方法
lazy 懒加载
今天我们来看看怎样通过变懒la💤y 😴…来提高效率⚡️
问题描述
1 | //假设你要做一个聊天应用,想用头像表示你的用户。针对每个头像你都要准备不同分辨率的版本,所以我们这样做: |
以上代码的弊端:我们需要在 init 中计算出 smallImage 的,因为编译器要求我们在 init 中初始化 Avatar 的所有属性。
但我们有可能会使用另一个小尺寸版本的用户头像,那么这个缺省值(smallImage)根本就不会被用到。也就是说,我们用计算量很大的图像缩放算法算出了这个缺省值,却没有任何卵用。
一种可能的解决方案
1 | //在 Objective-C 里,类似情况我们往往会使用一个中间私有变量(intermediate private variable),该用法翻译成 Swift 是这样: |
用这种方式,我们就可以随时给 smallImage 赋一个新值。但是如果在使用它之前没有赋过值,它不会返回 nil,而是基于 largeImage 计算一个值并返回。
这恰好满足我们的需求。但是代码实在太多。想想看,如果每个头像需要准备更多分辨率的版本,而且对每种版本都有这种需求,那该多恐怖!
Swift的惰性初始化(lazy initialization)
多亏了 Swift,我们可以省掉上面那些胶水代码(glue code),偷点儿懒… 只要简单的把 smallImage 变量声明成一个 lazy 存储属性即可!
1 | class Avatar { |
搞定了,使用 lazy 关键字,我们用更少代码实现了相同的行为!
如果我们在给 smallImage 惰性变量赋一个特定值之前使用了它,那么当且仅当此时该变量的缺省值才会被计算并返回。如果随后我们再次使用这个属性,它的值就已经被计算出来,会直接返回这个已存储的值。
如果我们在访问 smallImage 之前给它赋一个确切的值,那它就不会浪费时间计算那个缺省值,(随后访问它时)返回的是我们之前给它的那个确切的值。
如果我们永远不访问 smallImage 这个属性,那它的缺省值就永远不会被计算出来!
这就是一种可以避免无用初始化的有效且简单的方法,而且在没有使用中间私有变量的情况下提供缺省值。
用一个闭包做初始化
和其他属性一样,你可以用一个原地计算(in-place-evaluated)闭包来给 lazy 变量设定缺省值 - 使用= { /* some code */ }()替换掉= some code。当你需要多行代码去计算缺省值时,这么做更好。
1 | class Avatar { |
但由于它是一个 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 | // 全局变量,被以 lazy 形式(和一种线程安全的形式)创建 |
这段代码会先打印出 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 | func increment(x: Int) -> Int { |
对这段代码来说,在我们访问 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 | let array = Array(0..<1000) |
现在这段代码只打印出来这些
1 | Result: |
证明了只是在那些值被使用时才调用 increment 函数,而不是调用 map 的时候。并且只对那些被访问到的值使用,而不是对整个数组里面一千个值都使用!🎉
这下效率提高了很多!对那些涉及到庞大的序列(比如这个有 1000 个元素的数组)、以及高计算度闭包的情景来说,使用这个技巧会带来质变。
将惰性序列级联
有关惰性序列,最后一个小妙招就是你可以像 monad 那样,把高阶函数的调用拼接起来。比如你可以让一个惰性序列以这种方式调用 map(或者 flatMap):
1 | func double(x: Int) -> Int { |
这样只有当 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等等。
本文作者: NUK
最后更新: 2023年12月13日 17:35:03
本文链接: http://www.uilucky.com/post/fbc2ef68.html
版权声明: 本作品采用 CC BY-NC-SA 4.0 许可协议进行许可,转载请注明出处!