進行 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時,不妨想著:「啊,我現在是在放置一個代理人。」你會覺得這個模式親近許多。

