伯乐在线 nathanw 推荐本文,
Above 认领翻译 。
本文GitHub地址:GitHub
---------------------
“懒”起来
今天我们来看下怎么通过“懒”来更有效率。
具体的说,我们会讨论变量懒加载和序列的懒加载。还有喵。
问题
如果你开发了一个聊天app并且想要使用头像来代表用户。每个头像你可能有几种分辨率,我们使用以下方案来完成:
extension UIImage {
func resizedTo(size: CGSize) -> UIImage {
/* Some computational-intensive image resizing algorithm here */
}
}
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
,因为编译器强制要求我们在Avatar
的 init
方法中初始化所有的属性。
但是我们或许不会用到这个默认值,因为我们自己会提供小版的用户头像。所以我们白费功夫地使用计算密集型图片缩放算法计算了这个默认值。
合理的解决方案
在Objective-C中,(应对)这种情况有一个技巧是,我们经常会使用一个中间的(intermediate)私有变量,这个技巧转换为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
属性设置一个新的值,但是我们在smallImage
属性未赋值的情况下访问它,它会从largeImage
计算生成一个图片而不是返回nil
。
这确实是我们需要的,但是这也需要写太多代码。设想一下,如果我们想要这个方案适用于超过两种的所有不同分辨率!
Swift懒加载
感谢swift,现在我们只需要将
smallImage
变量声明为懒加载的存储属性就可以偷懒一下,避免上面这些“胶水代码”。
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
属性,这个默认值同样不会被计算。
所以这是一个很棒并且很轻松的办法来避免无用的初始化,而且同样提供了默认值,没有使用中间私有变量。
使用闭包初始化
对于其他属性,你也可以使用闭包,使用
= { /* some code */ }()
代替刚才的 = some code
. 当你需要多行代码计算默认值时这很有用。
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
}
}
因为这是懒加载属性,你可以在这里使用self(记住即使你像之前示例的那样不使用闭包,也是可以使用self关键字的)。
实际上这个属性是懒加载的,意味着默认值会延迟计算,在这个时候,self已经被完全初始化了,这就是为什么在这里能够访问self-这和当你给不是懒加载的属性设置默认值的时候相反,(非懒加载属性)是在初始化时候被赋值。
ℹ️ 立即应用的闭包,像上面用到的
lazy
变量默认值,是自动带 @noescape
.标签的的。这意味着在闭包里面不需要使用[unowned self]
标识,它们不会产生循环引用。
懒加载常量?
在Swift中,你不能创建实例的
lazy let
属性来提供只有访问时才计算的常量。
这是因为
lazy
的实现细节要求属性是可更改的,因为它初始化时没有值,之后访问的时候被赋值1。
但是我们谈论的是常量,常量的一个有趣特性是当它被声明为全局属性或者类型属性(使用
static let
关键字,不是实例属性)时是自动懒加载的(并且线程安全)2:
// Global variable. Will be created lazily (and in a thread-safe way)
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
常量是只有访问时才被创建的,不是在此之前(创建)3。
不要被类或结构体中的实例属性这种情况迷惑,如果你声明了一个
struct Foo { let bar = Bar() }
那么当 Foo
实例被创建时仍会立即计算bar
实例属性,并没有懒加载。
另一个例子:序列
让我们另外举一个例子,这次使用序列/数组和另外的高阶函数4,比如
map
:
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
这样的简单函数而是用计算密集型函数!
懒加载序列
让我们使用另外一种懒加载来改写上面的代码。
在swift标准库中,
SequenceType
和 CollectionType
协议 有一个名为 lazy
的计算属性,它们分别返回特殊的 LazySequence
或 LazyCollection。
这些类型是map、
flatMap
、filter等
类似的高阶函数专用的,通过“lazy“关键词使用5。
让我们在实例中看一下它是怎么工作的吧:
let array = Array(0..<1000)
let incArray = array.lazy.map(increment)
print("Result:")
print(incArray[0], incArray[4])
现在代码的打印如下:
Result:
Computing next value of 0…
Computing next value of 4…
1 5
表明只有当值被访问的时候才会调用
increment
方法,而不是访问map
方法的时候,并且只有被访问的值才会调用指定的方法,不是整个1000个元素的数组的所有的值。
这样效率更高!特别是大的序列(如拥有1000个元素的)和计算密集的闭包情况下,更加的明显。6
链式懒加载序列
懒加载序列还剩一个漂亮技巧是你可以结合高阶函数就像使用 monad一样。例如你可以像下面这个样,在一个懒加载序列上调用
map
(或者flatMap
):
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])
这只会在条目被访问时计算
double(increment(array[3]))
,而不是在访问它之前,并且只计算被访问的数据。
相反,使用
array.map(increment).map(double)[3]
(不使用lazy
)会先计算array所有的输出值,当所有的输出值被计算完毕后,取出第四个。但是更糟糕是,它会遍历array两次,每次调用map都会遍历!这将会是对计算时间的极大浪费!
结论
“懒”起来7。
1.在swift mail lists 仍有关于如何修复并支持懒加载常量一些讨论,但是目前为止对于swift2是这样的做法
2.记住在playground或者REPL,由于代码类似于在大的main()函数中执行,在最外围声明一个常量并不是全局常量并且不能实现这个效果,不要被playground或者REPL蒙蔽,在全局变量真正懒加载的真实的项目中尝试。
3.顺便说下,在类中使用static let是实现单例的推荐方式(虽然你应该避免使用它们),因为 static let 既懒加载又线程安全,并且只创建一次。
4.高阶函数是使用其他函数作为参数或者返回一个函数(或两者兼有)的函数, 高阶函数的示例是 map、flatMap、filter等.
5. 实际情况下,这些类型只保留对原始序列的引用和需要应用的闭包的引用,并且只有当元素被访问时才会应用闭包执行实际计算。
6.但是请注意-至少从我的实际体验来说-被计算得到的值并没有缓存(没有使用memoization对函数返回值进行缓存)所以你再次请求incArray[0]会再次计算,我们不能两全其美(目前为止)。
7.是的,由于太懒我没有写结尾。但是类似文章中展示的,变懒会让你称为好的程序员,对吧?