进行 iOS 开发时,经常会使用 lazy var 这一行。
我也一样。需要延后初始化重量级对象时,我总是习惯性地加上它。
后来我突然好奇:lazy其实不就是设计模式教材中的 Proxy Pattern 吗?
先说结论。
lazy是语言内置版本的虚拟代理(Virtual Proxy),会代替对象创建,直到真正需要该对象时才完成创建。
今天我会按照亲自操作代码、逐步理解的顺序,讲解 Swift 的 Proxy Pattern,以及这个概念如何隐藏在lazy关键字之后。
先快速总结重点。
- 代理模式是一种结构型模式,通过在真实对象前放置代理来控制访问。
lazy与延后创建对象的虚拟代理具有相同目的。- 不过,
lazy无法处理访问控制或日志记录等功能,只负责延迟创建。 - 如果需要日志记录或权限检查,就必须自己创建代理对象。
Swift 的代理模式到底是什么?
Proxy 就是代理人的意思。
在负责实际工作的对象前面,放置一个具有相同接口的代理人。
从外部看起来像是在调用真实对象,但实际上会先经过代理人。
代理人可以直接转发请求,也可以在中间做额外处理,例如检查权限、记录调用,或延后创建真实对象。
典型用途有三种。
- 虚拟代理:将重量级对象的创建延后到实际使用时
- 保护代理:阻止未经授权的访问
- 日志代理:记录调用历史
今天的主角是第一种,也就是虚拟代理。它正好与lazy对应。
隐藏在 lazy 背后的代理对象本质
想象一下处理高分辨率图片的场景。
图片加载很耗费资源。还没显示在屏幕上就提前全部加载,会浪费内存。
于是就有了“真正需要时再加载”的想法。这就是虚拟代理的核心。
先用代理模式直接实现,代码如下。
protocol Image { func display() }
// 代理人:将真实对象的创建延后到需要时
final class ImageProxy: Image {
private let filename: String
private var real: RealImage? // 尚未创建
init(_ filename: String) { self.filename = filename }
func display() {
if real == nil { real = RealImage(filename) } // 此时创建
real?.display()
}
}
只有第一次调用display()时,才会创建RealImage。
在此之前,ImageProxy只带着文件名安静等待。这正是代理人的工作。
不过,Swift 已将这个模式融入语法中,就是lazy。
final class Gallery {
// 第一次访问此属性时只创建一次
lazy var cover: RealImage = RealImage("cover.png")
}
let g = Gallery() // 尚未 RealImage 创建
g.cover.display() // 在这里才创建
创建Gallery()时,RealImage还不存在。
第一次访问g.cover时才会创建。也就是说,编译器替你生成了上面ImageProxy所执行的延迟创建。
那么,只要有 lazy,就不需要代理模式了吗?
我一开始的疑问也是这个。
答案是“不是”。
lazy只能代替你完成延迟创建。
它无法阻止访问、记录调用,也无法根据条件返回不同对象。
我把两者的区别整理成了表格。
| 项目 | lazy 属性 | 手动创建的代理 |
|---|---|---|
| 延迟创建 | 可以 | 可以 |
| 访问权限检查 | 不可以 | 可以 |
| 调用日志与缓存 | 不可以 | 可以 |
| 代码量 | 一行 | 一个类 |
| 可复用性 | 仅限该属性 | 可在多处复用 |
所以判断标准很简单。
如果纯粹只是“延后创建”,lazy一行代码就是答案,没必要专门创建一个类。
但如果需要检查权限、记录日志,或封装远程调用等中间处理,就必须自己建立代理对象。
还有一点需要注意。
lazy不保证线程安全。
如果多个线程同时进行第一次访问,初始化可能会发生两次。在多线程环境中,最好用自定义代理封装这部分并自行处理同步。
常见问题总结
Q. lazy 和计算属性(computed property)有什么区别?
lazy只在第一次计算并保存值。计算属性则会在每次访问时重新计算。如果希望重量级初始化只执行一次,应使用lazy。
Q. lazy 为什么只能声明为 var?
因为初始化发生在之后,所以在值确定前,实例会短暂存在。let不允许这种情况,因此lazy let在语法上不可能。
总的来说,lazy就像一份礼物:语言预先封装了代理模式中的虚拟代理。
一行代码就能完成的事情,不必特意创建代理类;但理解这一行背后的概念后,你看代码的方式会明显不同。
从今天开始,使用lazy var时,不妨想一想:“啊,我现在是在放置一个代理人。”这样会觉得这个模式亲切得多。

