Swift 與 Objective-C

[Swift 基礎 #1] Optional 的本質與解除包裝準則

學習 Swift 時首先遇到的關卡就是 Optional。String? 的問號、if let、guard let、!、??。語法種類很多,如果分別背誦,很容易把規則記得一團亂。

閱讀 7 分鐘
[Swift 基礎 #1] Optional 的本質與解除包裝準則 封面圖

學習 Swift 時首先遇到的關卡就是 Optional。String? 的問號、if letguard let!??。語法種類很多,如果分別背誦,很容易把規則記得一團亂。

不過,有一個事實能一次整理這些語法:Optional 不是特殊語法,而只是一個 enum。它是標準函式庫定義的普通型別,而問號只是這個型別的別名。了解它的本質後,所有 Optional 相關語法都會開始呈現為同一個原理的衍生形式。

本文是 Swift 基礎系列第 1 篇,一次整理 Optional 為何存在(哲學)、實際是什麼(實作),以及何時該使用哪種解除包裝方式(實務準則)。

在函式入口處解除 Optional 包裝的下一個步驟,請接著閱讀 Swift guard 與提早離開的選擇準則

為什麼存在 — 將「沒有值」提升為型別

Optional 的存在理由是哲學系列第 1 篇所談安全優先原則的代表案例,因此這裡只點出重點。

在大多數語言中,「沒有值」(null、nil)都可能混入任何參考。即使把 null 傳給預期接收 String 的函式,型別系統也不知道。因此 null 檢查只能交給文件與慣例,也就是人的記憶力;而每次記憶出錯的地方,都會產生 NullPointerException 與當機。

Swift 的解法,是把「可能沒有值」刻進型別。String 一定有字串,String? 則可能沒有。兩者完全是不同型別,不能直接互相指派。要使用可能不存在的值,必須經過「確認是否存在的程序」;省略這個程序就無法編譯。nil 檢查已從人的記憶力移交給編譯器。

本質 — Optional 是具有兩個 case 的 enum

在標準函式庫中查找 Optional 的宣告,會看到如下形式(這是簡化版)。

enum Optional<Wrapped> {
    case none
    case some(Wrapped)
}

就只有這些。「沒有值」的 none,以及「有值,而且值就是這個」的 some(Wrapped)。我們使用的所有語法,都是這個 enum 的語法糖。

  • String?Optional<String> 的縮寫表示法。
  • nilOptional.none 的別名。
  • var name: String? = "Kim" 實際上裝著 Optional.some("Kim")

因此 Optional 常被比喻成「盒子」。String? 不是字串,而是一個可能裝著字串、也可能是空的盒子。當然不能對盒子呼叫 .count,因為盒子不是字串。所有稱為解除包裝的行為,歸根究柢都是「打開盒子取出內容」;用 enum 的說法,就是透過模式比對取出 some case 的關聯值。

實際上,if let 是 switch 模式比對的縮寫。

let name: String? = fetchName()

// 原始形式: enum 模式比對
switch name {
case .some(let value): print(value.count)
case .none: print("沒有名稱")
}

// 縮寫: if let
if let value = name {
    print(value.count)
}

兩者是相同的程式碼。當 Optional 語法讓你覺得困難時,回到 enum 的原始形式思考,通常就能解決問題。

五種解除包裝工具,各有適用的位置
五種解除包裝工具,各有適用的位置

解除包裝工具箱 — 五種方法與各自的使用時機

開啟 Optional 的方法很多,容易混淆,但每種方法都有適合的位置。以下以實務標準整理。

**1. if let — 只在有值時暫時使用。**當有值時的處理會在該區塊內結束,就使用它。Swift 5.7 起,可以將 if let name = name 縮短為 if let name(SE-0345)。

**2. guard let — 沒有值就提早離開。**在函式開頭檢查前提條件,通過後直到函式結束都能使用解除包裝的值。成功路徑不需要增加縮排,這是它的優點,因此在實務函式程式碼中比 if let 更常見。

func register(email: String?) {
    guard let email else {
        print("需要電子郵件")
        return
    }
    // 從這裡開始 email在 String, 函式結束前都有效
    sendVerification(to: email)
}

3. nil 合併運算子 ?? — 有預設值時。「沒有就使用這個值」一行就能完成。let title = inputTitle ?? "제목 없음"

**4. Optional chaining ?. — 沒有值時就直接略過。**如果中途出現 nil,例如 user?.profile?.imageURL,整體會短路為 nil。只要記住結果型別也會是 Optional 即可。

**5. 強制解除包裝 ! — 沒有值就應該終止。**這是未經檢查就開啟盒子的語法,盒子為空時會立即當機。雖然常被說成「很危險,絕對禁止」,但更精確的準則是:只有在 nil 明顯代表程式設計師的錯誤,立即當機比悄悄繼續更好的位置才使用它。例如讀取應該一定包含在 App bundle 中的資源。相反地,在網路回應、使用者輸入、字典查詢等 nil 屬於正常情境的地方使用 !,確實就是一顆定時炸彈。

再補充一點。知道 Optional 是 enum 後,map 也很好理解。像 imageURL.map { download($0) } 一樣,也有不打開盒子就對內容套用函式的工具。沒有值時則什麼都不會發生。

隱式解除包裝的 Optional — 使用驚嘆號而非問號的型別

也有宣告中帶驚嘆號的 String!。這是隱式解除包裝的 Optional(IUO),也就是「雖然是 Optional,但每次使用時都會自動強制解除包裝」的型別。

它存在是為了解決初始化時機問題。Storyboard 的 @IBOutlet 就是代表例子。View controller 建立時,outlet 尚未連接,因此是 nil;畫面顯示後則一定有值。這是為了處理這種尷尬情況的折衷:每次解除包裝很麻煩,但又不能說它不是 Optional。

實務準則很簡單:除了框架要求的地方(例如 IBOutlet)之外,最好不要新建它。初始化順序問題大多可以透過 lazy 或相依性注入更安全地解決。

在邊界解除 Optional,讓確定的值流入領域內部
在邊界解除 Optional,讓確定的值流入領域內部

面對 Optional 的設計感

知道語法與善用語法是兩回事,因此補充三項設計層面的準則。

**不建立 Optional 是最好的選擇。**只有在「可能不存在」確實為真時,Optional 才有價值。明明永遠有值,卻習慣性加上 ?,只會讓毫無意義的解除包裝儀式蔓延到所有使用處。宣告屬性時問自己一次「它實際上真的會有 nil 的時刻嗎?」,程式碼就會大不相同。

**在邊界解除 Optional,不要把它帶進內部。**在網路、使用者輸入、字典查詢等外部邊界,Optional 無可避免。良好的結構會在邊界函式中用 guard let 整理,讓只有確定的值流入領域邏輯內部。如果內部函式簽章充滿 Optional,就表示解除包裝的位置太深了。

**如果「沒有」有多種意義,Optional 就不夠了。**如果必須區分「尚未載入」、「載入但失敗」與「原本就不存在」,就應該直接建立帶有這些意義的 enum,而不是使用 Optional。了解 Optional 是 enum 後,就能用相同方式設計符合自身情境的 enum。

直接執行確認的結果

2026 年 8 月 26 日,我在 Apple Swift 6.3.3(arm64-apple-macosx26.0)中,將 .some(42).none 建立為相同的 Optional<Int>,並執行了 nil 合併運算子。

optional=some:42,none-fallback:0

比起只閱讀語法說明,直接建立 .some.none 並放入相同的程式碼路徑,可以清楚看出 Int? 並不是額外的魔法,而是具有兩種狀態的型別。在實際 App 中,確認後要決定使用 ??,還是以 guard 拒絕。我的判準是:值不存在是正常的預設值,還是代表無法繼續的輸入錯誤。

總結

  • Optional 不是特殊語法,而是具有 case nonecase some(Wrapped) 的 enum。String?nil 與 if let 全都是這個 enum 的語法糖。
  • 它存在的理由,是將「可能沒有值」提升為型別,讓編譯器強制執行 nil 檢查。
  • 解除包裝各有適用時機:短暫使用時用 if let,提早離開時用 guard let,預設值用 ??,想略過時用 ?.,只有發生錯誤就應該當機的位置才用 !。
  • 設計感:不要建立不必要的 Optional,在邊界解除包裝;如果「沒有」有多種意義,就建立專用 enum。

下一篇是基礎系列第 2 篇:Closure。內容包括 Closure 為何是參考型別、Capture list 精確複製了什麼,以及為何需要 escaping。

延伸閱讀

來源與驗證