var name = ""。這是 Swift 中最常見的一行,但能放在這裡的選項比想像中更多:儲存屬性、計算屬性、lazy、willSet 與 didSet,以及型別屬性。即使學會了所有語法,真正問到「這個值該用 lazy 還是計算屬性?」時,仍常常抓不到判斷標準。
這是 Swift 基礎系列第 3 篇。本文不羅列語法,而是以選擇準則整理五種屬性。核心問題只有一個:這個值會被儲存、被計算,以及何時建立?
在因為需要共用狀態而選擇型別屬性之前,也一起確認 Swift 單例的測試成本,就能更清楚看出 static let 與 static var 的界線。
儲存 vs. 計算——記憶體中的值與當下產生的值
屬性的第一個分岔是儲存還是計算。
儲存屬性(stored property)是實際佔據執行個體空間的值。寫成 var name = "Kim" 時,執行個體記憶體會配置 name 的空間。struct 的大小由儲存屬性的總和決定。
計算屬性(computed property)沒有儲存空間。每次存取時執行程式碼產生值,實際上就像函式。
struct Rectangle {
var width: Double // 儲存
var height: Double // 儲存
var area: Double { // 計算——沒有儲存空間
width * height
}
}
你也可以把 area 做成儲存屬性。但如此一來,每次 width 改變都要更新 area,兩者不同步就會變成錯誤。這導出第一個準則:由其他值推導出的值不要儲存,改用計算。維持單一真實來源,就能從根本消除同步錯誤。
在計算屬性加上 set 就能寫入。儲存 celsius、將 fahrenheit 設為計算屬性,對 fahrenheit 賦值時就反向計算並更新 celsius。此時真正儲存的值仍只有一個,其餘都是觀看它的視窗。
也說明一下何時該用計算屬性取代函式。Apple 的 API 設計準則慣例是:計算成本低(通常接近 O(1))、沒有副作用,且概念上是「物件的屬性」時用屬性;計算昂貴或有副作用時用方法。這就是 array.count 是屬性、array.sorted() 是方法的原因。
lazy——第一次使用時建立的儲存屬性
lazy 是第三種選項。它是儲存屬性,但不是在建立執行個體時初始化,而是在第一次存取時才初始化。
class ImageProcessor {
lazy var filters: [Filter] = loadExpensiveFilters()
}
常見用途有兩種。第一,初始化成本高但可能不會使用的值;每次建立執行個體都準備沉重資源很浪費。第二,需要參照 self 的屬性。一般儲存屬性的預設值會在 init 結束前計算,因此不能使用 self;lazy 將初始化延後到第一次存取,所以允許參照 self。這也是立即執行閉包(= { ... }())常與 lazy 搭配的原因。
有兩點要注意。lazy 必須是 var。因為它的值會從類似 nil 的未初始化狀態變成實際值,所以不能是 let。此外它不是執行緒安全的。多個執行緒同時第一次存取時,初始化可能執行兩次。多執行緒環境中若必須只建立一次,就需要其他機制。
它與計算屬性的差異也很明確:lazy 計算一次並儲存,之後回傳該值;計算屬性則每次重新計算。「昂貴但不變的值」用 lazy,「便宜但必須永遠最新的值」用計算屬性。
willSet 與 didSet——回應值的變更
儲存屬性可以加上變更觀察器。在值變更前(willSet)與變更後(didSet)執行程式碼。
class ProgressBar {
var progress: Double = 0 {
didSet {
// oldValue會自動提供
guard progress != oldValue else { return }
updateUI()
}
}
}
典型用途是在值變更時自動處理後續工作,例如 UI 更新、記錄,以及值驗證(超出範圍就復原)。不用自行建立 setter,也能保留賦值語法並加入附帶動作。
實務上有兩個容易混淆的規則。第一,在 init 中賦值時不會呼叫觀察器。初始化是「設定」而不是「變更」,因此 didSet 中的 UI 更新不會套用到初始值。第二,修改值型別的屬性時,包含它的屬性之 didSet 也會被呼叫。如 person.name = "Lee",即使只修改 struct 內部,也會呼叫 person 的 didSet。值型別章節提到的「修改就是替換成新值」語意,在這裡同樣成立。
要小心濫用 didSet。若其中堆積太多邏輯,就很難追蹤一行賦值究竟做了什麼。改一個值卻送出網路請求,便違反最小驚訝原則。didSet 只放輕量同步,沉重邏輯移到明確的方法中會更安全。
型別屬性——繫結在型別而非執行個體上的值
加上 static 後,屬性會繫結到型別本身,而不是執行個體。
struct APIConfig {
static let baseURL = URL(string: "https://api.example.com")!
static var requestCount = 0
}
無論建立多少執行個體,型別屬性都只有一份。它適合常數集合(設定值、共用格式化器),標準函式庫也常在 Int.max 或 Double.pi 這類位置使用。
有一點值得知道:static let 保證具備執行緒安全的延遲初始化。第一次存取時只初始化一次,並且能安全處理並行存取。lazy var 沒有這項保證,static 卻有。這也是單例的 static let shared 能在沒有額外鎖定機制下成立的依據。不過可變的 static var 狀態實際上就是全域變數,會破壞測試隔離,也可能成為資料競爭來源。單例為何被稱為反模式會在另一篇文章討論;這裡只留下「static var 是最後手段」這項準則。
選擇流程圖——用四個問題整理
把五種類型濃縮成一句話問題,就是以下這樣。
- 是否由其他值推導而來? → 計算屬性。只儲存一個真實來源。
- 初始化成本高,或需要 self 嗎? → lazy var。但要小心多執行緒的第一次存取。
- 是否有跟隨值變更的動作? → 儲存屬性 + didSet。但只放輕量工作。
- 是否不依賴執行個體、只需要一份? → static。若是 let,還能獲得安全的延遲初始化。
- 如果都不是 → 一般儲存屬性。多數情況下,這就是答案。
SwiftUI 的 @State、@Published 等屬性包裝器(property wrapper),最終也是建立在這套屬性系統上的語法。了解儲存、計算與觀察器的原理,就能看出包裝器其實是「包住儲存屬性,自動化 didSet 類行為」。直接建立屬性包裝器會在中級系列介紹。
直接執行確認的結果
我在 Apple Swift 6.3.3 中,於同一個物件記錄 lazy 初始化次數、didSet 的前後值,以及計算屬性的結果。輸出顯示讀取 lazy 屬性兩次,並將儲存屬性從 0 改為 7。
properties=lazy-builds:1,didSet:0->7,computed:14
正因這個小型執行結果,如果快取真的必須只建立一次,我不會只相信 lazy 這個語法。只要可能並行存取,就測試初始化次數,確認是否需要額外同步。相反地,didSet 只放像上述輸出那樣觀察狀態變化的輕量動作,可能失敗的 I/O 則移到明確的方法。
總結
- 屬性的第一個準則是儲存還是計算。由其他值推導出的值使用計算,維持單一真實來源。
- lazy 是第一次存取時初始化的儲存屬性,可解決昂貴初始化與 self 參照問題。代價是必須使用 var,且不具執行緒安全性。
- willSet/didSet 用來回應值的變更,但在 init 中不會呼叫;若放入沉重邏輯,也會難以追蹤。
- static let 是保證執行緒安全延遲初始化的型別屬性;static var 是全域狀態,應作為最後手段。
下一篇是基礎第 4 篇:guard。我們會深入解析在 Optional 文章中短暫介紹的提早離開,整理 Swift 社群為何與縮排展開戰爭。
推薦閱讀
- Swift Optional 的真相:其實是 enum(含 5 種實務解包準則)
- [Swift 基礎 #4] 正確使用 Swift guard:以提早離開放倒毀滅金字塔
- [Swift 基礎 #5] Swift 錯誤處理完整地圖:throws、try?、try!、Result 何時使用?
來源與驗證
- The Swift Programming Language: PropertiesSwift.org · 官方文件 · 查核 2026年8月26日依據: 儲存、計算、延遲儲存、屬性觀察器與型別屬性的語言規則

![[Swift 基礎 #3] Swift 屬性總整理:儲存、計算、lazy、didSet 的 4 個選擇準則 封面圖](/assets/images/posts/7621582a-1bb6-4f5a-8fca-12fae48417af/1.jpg)