“懒”点儿好

作者:Olivier Halligon,原文链接,原文日期:2016-02-28
译者:ray16897188;校对:numbbbbb;定稿:星夜暮晨

今天我们来看看怎样通过变懒la💤y 😴...来提高效率⚡️。具体点儿说,我们要聊一聊 lazy varLazySequence 和喵星人😸。

问题描述

假设你要做一个聊天应用,想用头像表示你的用户。针对每个头像你都要准备不同分辨率的版本,所以我们这样做:

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)根本就不会被用到。也就是说,我们用计算量很大的图像缩放算法算出了这个缺省值,却没有任何卵用

一种可能的解决方案

在 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 存储属性即可!

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。当你需要多行代码去计算缺省值时,这么做更好。

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)的(还是线程安全的)2

// 全局变量,被以 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 initialized42,接下来是 Type constant initializedFelix,最后是 Bye;证明了 fooCat.defaultName 这两个常量只在被访问时才被创建,而非初始化时创建3

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

另一个例子:Sequences

我们再举一个例子,这次是 sequence(序列)/ Array,以及一些高阶函数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 更大会怎样!

Lazy sequences(惰性序列)

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

在 Swift 标准库中,SequenceTypeCollectionType 协议都有个叫 lazy 的计算属性,它能给我们返回一个特殊的 LazySequence 或者 LazyCollection。这些类型只能被用在 mapflatMapfilter这样的高阶函数中,而且是以一种惰性的方式。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 个元素的数组)、以及高计算度闭包的情景来说,使用这个技巧会带来质变。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])

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

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

结论

正如那句话所说的:“懒惰是人类文明进步的真正动力”


  1. Swift 的邮件列表中还在讨论如何解决相应问题并使 lazy let 成为可能,但是在 Swift 2 中还是无法使用。

  2. 请注意,在 playground 或者 REPL 中代码的执行环境类似一个 main() 函数,因此最外层声明的 let foo: Int 不会被看做是一个全局常量。因此(在 playground 或者 REPL 中)你看不到类似的结果。别被 playground 或者 REPL 骗了,实际项目中的那些 let 的全局常量确实是 lazy 的。

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

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

  5. 实际上,这些类型其实就是保留了一个对“原序列”的引用,又保留了一个对“待调用闭”的引用,然后只在某个元素被访问时再对这个元素调用该闭包,做出实际的计算。

  6. 但请知悉 - 至少以我自己实验的结果 - 计算出的返回值并没有被缓存(memoization);所以如果你再次调用incArray[0],结果就又被计算一次。鱼与熊掌不可兼得...(目前)

  7. 对,我太懒了就没写结论。但是就像这篇文章说的那样,对懒的追求可以将你造就成一个好的程序员,对吧?😜

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容