Swift 與 Objective-C

[Swift 哲學 #1] Safe・Fast・Expressive

使用 Swift 撰寫程式碼時,你總會有些疑問:為什麼 Optional 這麼嚴格?為什麼陣列會被複製?為什麼有 guard?逐一深入這些問題,最後會遇到 Swift 從一開始就提出的三個目標:Safe(安全)、Fast(快速)、Expressive(具表達力)。

閱讀 8 分鐘
[Swift 哲學 #1] Safe・Fast・Expressive 封面圖

使用 Swift 撰寫程式碼時,你總會有些疑問:為什麼 Optional 這麼嚴格?為什麼陣列會被複製?為什麼有 guard?逐一深入這些問題,最後會遇到 Swift 從一開始就提出的三個目標:Safe(安全)、Fast(快速)、Expressive(具表達力)。

Swift 官方網站(swift.org)在語言介紹開頭明確列出這三點。這不只是行銷文案。過去十多年加入 Swift 的幾乎每項功能,都以這三個標準接受檢驗。本文整理三種哲學如何化為語言功能,以及彼此衝突時 Swift 選擇了哪一方。

本文是 Swift 哲學系列的第一篇。Swift 誕生的背景,也就是 Chris Lattner 為何捨棄 Objective-C,已在另一篇文章中介紹;這裡聚焦於「誕生後依循哪些原則成長」。

Safe — 讓編譯器預先阻止錯誤

安全是 Swift 設計中優先級最高的價值。這裡的安全,意思是「讓人難以犯錯」。不依賴程式設計師的善意或專注力,而是在語言層級堵住犯錯的途徑。

代表例子就是 Optional。在 Objective-C 時代,任何指標都可能是 nil,對 nil 傳送訊息時會被靜默忽略。不會當機,卻很難追查錯誤從哪裡開始。C 系列語言則會直接當機。Tony Hoare 稱 null 參考為「價值十億美元的錯誤」,已是廣為人知的故事。

Swift 將這個問題提升到型別系統。可能沒有值的變數,必須在型別中加上問號,例如String?;沒有問號的型別絕不可能是 nil。要使用可能為 nil 的值,編譯器會強制你透過 if let 或 guard let 拆開它。

var name: String? = fetchUserName()

// 編譯錯誤 — 不能直接使用 Optional
// print(name.count)

if let name {
    print(name.count) // 這裡已安全保證
}

「忘了檢查 nil」的執行階段錯誤,變成了「沒有拆開 Optional」的編譯錯誤。錯誤在建置階段就會被攔下,不會到達使用者手中。

安全哲學不只存在於 Optional,也遍布各處。

  • 強制變數在使用前初始化:從根本阻止讀取未初始化記憶體的錯誤。
  • 陣列範圍檢查:索引超出範圍時,不會讀取異常記憶體,而是立即停止。
  • 偵測整數溢位:C 會靜默地讓數值翻轉,但 Swift 會在基本運算中觸發陷阱。
  • 有型別推斷,但沒有隱式轉換:不能直接將IntDouble相加。雖然麻煩,但相較於 C 的隱式型別轉換造成的細微錯誤,這是值得的。

有一點值得注意。Swift 的安全更接近「不存在未定義行為」,而不是「不會當機」。陣列索引超出範圍時,Swift 反而會刻意當機。與其帶著奇怪的值繼續執行,在問題點確實停止更安全。

Swift 的安全哲學會在編譯閘門預先攔截執行階段錯誤
Swift 的安全哲學會在編譯閘門預先攔截執行階段錯誤

Fast — 不為了安全而犧牲效能

安全的語言很多,問題是安全機制通常很昂貴:垃圾回收、執行階段型別檢查、直譯器。腳本語言過去以速度換取安全與便利。

Swift 的野心是拒絕這項交易本身。目標很明確:維持上述所有安全機制,同時達到接近 C 系列語言的效能。為此,它疊加了多層機制。

第一,盡可能在編譯時決定。Swift 是靜態型別語言,編譯器知道所有型別,因此能在編譯時將方法呼叫直接固定為位址(靜態派送)。這與 Objective-C 在執行階段透過 objc_msgSend 查找所有方法呼叫形成對比。類別加上 final 後能更積極最佳化,也是同樣的原理。

第二,使用 ARC(Automatic Reference Counting,自動參考計數)取代垃圾回收。參考計數會在編譯時插入 retain/release 程式碼,因此不像 GC(垃圾回收)那樣在執行階段暫停程式並清理記憶體。記憶體何時釋放也可預測,是另一項優點。

第三,值型別與以協定為中心的設計。struct 不需付出堆積配置與參考計數成本即可放在堆疊上;泛型經過特化後,會編譯成各型別專用的程式碼。使用抽象化不代表要支付執行階段成本,編譯器會剝除抽象化並產生具體程式碼。這稱為零成本抽象化。

當然,現實比目標複雜。Swift 仍有隱藏的效能成本,例如存放協定型別的 existential container,以及類別的參考計數負擔。因此,準確的理解不是「Swift 一定很快」,而是「語言開放了能夠快速執行的道路,離開那條路就要付出成本」。深入篇將詳述這條道路。

Expressive — 讓意圖原樣呈現在程式碼中

三者之中最難掌握的價值是表達力。簡單說,讀程式碼時應能直接理解作者的意圖,而且不必透過冗長儀式就能寫出想表達的內容。

與 Objective-C 比較時,這種感受尤其明顯。

// Objective-C
NSArray *names = @[@"Kim", @"Lee", @"Park"];
NSMutableArray *upper = [NSMutableArray array];
for (NSString *name in names) {
    [upper addObject:[name uppercaseString]];
}
// Swift
let names = ["Kim", "Lee", "Park"]
let upper = names.map { $0.uppercased() }

行數減少固然重要,但更重要的是意圖密度。map 一個詞就完整傳達「轉換每個元素並建立新陣列」的意圖;for 迴圈版本則需要讀者自行重建這個意圖。

支援表達力的機制,舉幾個例子如下。

  • 型別推斷:寫成let names = ["Kim", "Lee"]時,編譯器知道它是[String]。保留型別安全,只減少型別標註的雜訊。
  • enum 與關聯值:將「成功時是資料、失敗時是錯誤」的狀態直接建模為case success(Data)case failure(Error),讓狀態與資料不會分離。
  • 尾隨閉包、下標與運算子定義:讓函式庫能建立讀起來像語言語法的 API。
  • resultBuilder:SwiftUI 的宣告式語法就是用它建立的,UI 結構會與程式碼結構一致。

也有需要注意的地方。表達力不等於「短」。Swift API Design Guidelines 的第一原則是「使用位置的清晰度(clarity at the point of use)」;清晰度優先於簡潔。這就是為什麼存在像remove(at: 3)這種刻意加上引數標籤的語法。remove(3)雖然更短,讀者卻會疑惑它是刪除第三個位置,還是刪除值 3。

發生衝突時安全勝出,效能與表達力由編譯器取回
發生衝突時安全勝出,效能與表達力由編譯器取回

三者衝突時,誰會勝出

有三種哲學,就一定會出現衝突。Swift 的真正性格,會在這些衝突的判定紀錄中顯現。

安全 vs 表達力:Optional 拆開明顯讓程式碼更吵雜。如果像 Python 一樣直接使用,程式會更短。Swift 選擇了安全;同時透過 if let 縮寫語法、Optional chaining(user?.name)、nil 合併(??)等,在維持安全的同時減少雜訊,以表達力補償。

安全 vs 效能:陣列範圍檢查代表每次存取都要增加比較運算。Swift 預設選擇安全,但在編譯器能證明不可能超出範圍時,會移除檢查以取回效能。對真正需要的人,也提供了withUnsafeBufferPointer這類逃生口。名稱中寫入 unsafe,讓承擔風險的事實留在程式碼裡。

效能 vs 表達力:高階函式與泛型等抽象化是表達力的核心,但天真實作會很慢。Swift 透過內嵌與泛型特化投入編譯器能力,讓「使用抽象化後仍能產生與手寫程式碼相同的機器碼」。

看到這個模式了嗎?預設永遠是安全;效能與表達力則透過編譯器最佳化及明確的逃生口取回。Swift 幾乎所有設計決策都能用這個公式解釋。

這套哲學在實務上的意義

哲學可能讓人覺得抽象,但它與實務有直接關聯。

第一,與編譯器站在同一邊,而不是對抗它,會更有利。在 Swift 中,多數編譯錯誤都是「現在就抓到未來的執行階段錯誤」的訊號。因為 Optional 麻煩就濫用!,等於親手拆掉語言建立的防線。

第二,設計 API 時有了判準。如果自己建立的函式很容易被誤用,就不夠 Swift。設計型別,讓錯誤使用變成編譯錯誤,這是 Swift 風格的核心,也會在後續系列反覆出現。

第三,能理解新功能。async/await 同時針對 callback hell 的表達力問題與資料競爭的安全問題;Swift 6 的 strict concurrency 則延伸了「連並行處理錯誤也要在編譯時抓出來」的安全哲學。巨集在編譯時解決樣板程式碼的表達力問題。理解三種哲學後,每次新功能出現,都能看出它推進了哪項價值、採用了什麼方式。

總結

  • Swift 的所有設計,都源自 Safe、Fast、Expressive 三種價值的平衡。
  • Safe:Optional、強制初始化、範圍檢查,將錯誤變成編譯錯誤,而非執行階段錯誤。
  • Fast:靜態派送、ARC、值型別、泛型特化,在維持安全機制的同時追求 C 級效能。
  • Expressive:型別推斷、enum、閉包,目標不是短,而是意圖清晰。
  • 衝突時預設選擇安全,並透過編譯器最佳化與明確逃生口取回效能和表達力。

下一篇將介紹這套哲學如何反映在學習曲線上:一行 print 腳本與泛型函式庫能共存於同一語言的秘密——Progressive Disclosure。

延伸閱讀