Swift 標準函式庫有個醒目的特點:Int、Double、Bool、String、Array、Dictionary、Set 全都是 struct。在其他語言中是類別的型別,在 Swift 裡都是值型別。Java 的 String 是類別,而 Python 中一切都是物件參考。
這並非偶然。Swift 在設計階段就確立「預設選擇是值型別」的方針,並在 WWDC 2015 的知名場次「Protocol-Oriented Programming」與「Building Better Apps with Value Types」中正式宣示。本篇整理 Swift 為何以值而非參考為預設,以及這項選擇如何影響整個語言。
這是 Swift 哲學系列第 3 篇。類別與結構的語法差異已在其他文章介紹,本篇聚焦於「為什麼要做出這些差異」的設計意圖。
以參考為預設的世界,其根深蒂固的問題
要理解值型別優先,必須先看看以參考型別為預設的世界有什麼問題。
參考型別的本質是共用。即使複製變數,物件仍只有一個,兩個變數都指向同一物件。有意的共用是功能,非預期的共用卻是錯誤溫床。典型模式如下。
// 假設是參考型別(類別)
let settings = defaultSettings
settings.fontSize = 20 // 原本沒打算動到預設設定
// defaultSettings.fontSize卻 20了
以為複製了,其實卻在共用。這類錯誤棘手之處在於症狀與原因相距甚遠。值出問題的地方和破壞它的程式碼隔了好幾個檔案,除錯就變成「追蹤所有參考這個物件的地方」。
Objective-C 開發者很清楚這個問題,因此一直用慣例防範。他們將 NSString 屬性宣告為 copy,分離 NSArray 的可變版本(NSMutableArray)與不可變版本,並習慣加入防禦性複製。這些都是用開發者紀律補救「參考為預設」所造成問題的修補。
Swift 團隊的觀點是:如果每次都要靠慣例防範,問題會不會其實是語言的預設值錯了?
值型別守護的事 — 區域推理
值型別的核心特性是複製後會真正成為彼此獨立的個體。
var a = [1, 2, 3]
var b = a
b.append(4)
// a仍然是 [1, 2, 3]
這保證的不只是便利,而是區域推理能力。將陣列傳入函式時,如果是值型別,就不必擔心「這個函式可能偷偷改動我的陣列」。只要閱讀自己的程式區塊,就能完整掌握變數狀態。在參考型別的世界,必須從整個程式思考「誰持有物件、何時會修改」;值型別則把範圍縮小到眼前的一個函式。
這正好延續第 1 篇的 Safe 哲學。Optional 在編譯時捕捉「忘記處理 nil」,值型別則在型別層級消除「非預期共用」這類錯誤。用 let 宣告的 struct 也是真正不可變。類別實例的 let 只代表參考不變,內容仍可修改。
這項特性隨時間更有價值。在多執行緒環境中,資料競爭發生於多個執行緒共用同一段記憶體;值型別原本就不共用,因此競爭的前提消失。Swift Concurrency 將值型別列為能跨越執行緒界線的代表型別(Sendable),是自然結果。2014 年的設計決策,在 2021 年的並行處理模型中得到回報。
「複製不會很昂貴嗎?」— Copy-on-Write 的答案
對值型別優先的第一個質疑總是效能:每次把十萬個元素的陣列傳給函式都整份複製,真的負擔得起嗎?
Swift 的答案是 Copy-on-Write(CoW)。Array、Dictionary、Set、String 等標準集合在賦值時共用內部儲存,只有任一方修改的瞬間才真正複製。語意上是互不看見修改的完整值,成本上則像參考一樣,唯讀時不需複製。
var a = hugeArray // 不會複製,共用儲存
let x = a[0] // 仍然不複製
a.append(1) // 此刻才第一次複製
要注意 CoW 不是語言功能,而是函式庫實作技術。標準集合有內建,但不會自動套用到我們自行建立的 struct。若自訂值型別承載大型資料,就要用 isKnownUniquelyReferenced 自行實作。這項實作細節已在 CoW 專文整理。
反方向也有效能優勢。小型 struct 不需堆積配置或參考計數,就能放在堆疊上,因此反而比類別便宜。CGPoint 是 struct 的原因就在這裡。所以「值型別很慢」的直覺,在 Swift 中通常正好相反。
沒有繼承也能生存的方法 — 協定與組合
值型別優先的代價之一是 struct 不能繼承。沒有參考就難以實作部分多型;那麼程式碼重用與多型要怎麼辦?
Swift 的答案是協定導向程式設計(POP)。以協定宣告共用介面,將共用實作放進協定 extension,再讓型別採用多個協定來組合能力。繼承是從單一父類別承接一切的垂直結構;協定採用則是選擇所需能力的水平結構。
struct Player: Codable, Equatable, Comparable {
let name: String
let score: Int
static func < (lhs: Self, rhs: Self) -> Bool {
lhs.score < rhs.score
}
}
這個 struct 沒有繼承任何東西,卻具備 JSON 轉換、相等比較與排序能力。而且 Codable 和 Equatable 的實作還能由編譯器自動合成。值型別加協定的組合,取代了繼承大多數的實際用途。
所以值型別優先與協定導向是一組設計。WWDC 2015 將兩個場次並列發表並非偶然。「以 struct 與協定取代類別繼承」是 Swift 提出的基本組合,整個標準函式庫都依此建構。POP 本身另有專文詳述。
那麼,類別該在什麼時候使用?
值型別優先不代表「不要使用類別」。精確說法是:「預設使用 struct,有充分理由需要參考時才使用類別。」理由大致有三個。
**需要物件識別(identity)時。**資料庫連線、畫面上的 view、檔案控制代碼等,即使值相同也代表不同存在,使用參考很自然。兩個連線設定相同,也不會因此成為同一個連線。
**共用本身就是目的時。**多個畫面必須觀察同一狀態的共用模型,或整個 App 只能有一個的管理器,都把參考的共用特性當成功能。
**需要管理生命週期時。**必須透過 deinit 清理資源,或要與 UIKit 等 Objective-C 框架互動時,就使用類別。
Apple 官方文件的指南也採取相同方向:預設使用 struct 與 enum,符合上述條件時才選擇類別。實際上,SwiftUI 時代的 App 程式碼正逐漸收斂為 view 使用 struct、狀態資料使用 struct,以及少數參考模型使用 @Observable 類別。值是預設、參考是例外的哲學,已貫徹到 UI 框架層級。
總結
- Swift 標準函式庫幾乎全是 struct,這是設計方針。預設選擇是值型別。
- 目標是在型別層級消除參考為預設的語言中,因非預期共用造成的遠端錯誤。
- 值型別守住區域推理,而這項特性又在 Swift Concurrency 的 Sendable 中得到回收。
- 效能疑慮由標準集合的 Copy-on-Write 與小型 struct 的堆疊配置來回答。
- 繼承的空缺由協定導向程式設計填補,兩者是成套設計。
- 類別被重新定位為在需要識別、共用或生命週期管理時使用的工具。
Swift 哲學系列至此介紹了安全性(第 1 篇)、學習曲線(第 2 篇)與值型別(第 3 篇)。下一篇將探討這些哲學實際反映到語言中的 Swift Evolution 流程,也就是一項語法從取得 SE-XXXX 編號到進入語言的旅程。

![[Swift 哲學 #3] Swift 為什麼全部都是 struct?值型別優先主義總整理 封面圖](/assets/images/posts/c6e5417c-1bad-4913-bdff-974f972e1a73/1.jpg)