lazy in swift

面临的问题:

开发一个聊天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)
  }
}

上述代码的问题是,smallImage是在初始化的时候生成的,因为编译器要求我们必须在构造函数初始化所有属性。但是smallImage可能永远不会被用到(比如分享的时候才用),那么生成smallImage就浪费了很多资源,而且会增加实例初始化的时间。

解决办法

Objective-c中有一种常用的解决办法是使用一个中间的私有变量,用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属性,那代码会非常的多。

Swift lazy initialization

感谢Swift,我们可以通过使用Swfit的lazy stored property特性来实现上面的需要,而避免写那么多的代码。

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
  }
}
  • 如果没有给smallImage赋值,而且只有这种情况下,默认的值会被计算并返回。下次你访问smallImage属性的时候,之前计算得到值会直接被返回
  • 如果在访问smallImage之前显性的赋一个值,那么访问smallImage的时候,直接返回显性赋的值,默认内容不会被计算
  • 如果我们从不访问smallImage,那么默认内容绝不会被触发

这是一种很赞并且简单的办法避免那些无用的初始化!

使用闭包lazy初始化

如果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
  }
}

因为smallImage是个lazy属性,这里可以使用self的引用(即便你不适用闭包生成lazy属性,前面的一个例子就是这么做的)

如果一个属性是lazy的,他的默认内容只有在实例初始化完毕后才会被执行,由于实例已经完全初始化完毕了,self当然是可以访问的。  相反非lazy属性必须在构造器里初始化,那个时候实例还没有初始化完毕,self是不可访问的。  注意:! 使用闭包生成lazy 属性的时候,闭包自动被赋予了@noescape属性,也就是说你没必要在这个闭包中使用[weak self]或者[unowned self],直接使用self在这里并不会造成循环引用导致内存泄露。

lazy let?

在swift中,你无法通过lazy let创建实例属性,因为lazy属性的实现细节:属性创建的时候没有被初始化,后面被赋值或者被初次访问的时候才回初始化它的值。

但是swift的一个有趣的特性是,他可以通过let创建全局变量 和 类型常量, 这两中类型的常量是自动具备lazy的特性(而且是类型安全的),而不需要使用lazy关键词。

// 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”常量只有在访问的时候才被创建。

实例常量并不会自动赋予lazy属性,它必须在声明时或者构造器里初始化。

Lazy sequences

来看一个sequences的例子:

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之前,所有的值都被计算了,所以你可以看到1000个“ Computing next value of …”输出。但是我们只需要第0个和第4个值,其它值我并不关心,可以不计算他们吗?

当然了,使用Lazy sequences

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

表明incArray被访问的时候,值才被计算,这次并没有在map参数被赋值increment的时候计算所有incArray的值.这种做法更高效,避免了无意义的计算.

最后一个技巧是Chaining lazy sequences, 先看例子

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]))被访问的时候,这个值才会被计算,而且只有这一个值会被计算.如果不使用lazy,直接使用array.map(increment).map(double)[3]会计算array.count的平方次,这是一种很糟糕的做法.

 

感谢原文:Being Lazy