map 和 filter 大家都會用,問題在接下來的部分。compactMap 和 flatMap 名稱相近,容易混淆;reduce 的簽章又讓人望而卻步。鏈式呼叫拉得太長時,還會不斷冒出「這到底重新建立了幾次陣列?」的效能疑慮。
中級系列第 5 篇是高階函式實戰整理,涵蓋五個函式的精確區別、與 for 迴圈的選擇標準,以及 lazy 序列能解決的問題。這是建立在閉包篇基礎上的實務篇。
五個函式的精確定位——簽章告訴你的事
高階函式(higher-order function)是以函式為引數的函式。陣列處理五大函式的區別,可由閉包的簽章整理出來,精確地說,就是「一個元素會變成什麼」。
map:元素 → 新元素。數量維持不變。 輸入 n 個元素,就會得到 n 個元素。只轉換,不篩選。
filter:元素 → Bool。數量可能減少,但元素本身不變。 只保留符合條件的元素,不進行轉換。
compactMap:元素 → Optional。丟棄 nil 並拆開回傳。 它是 map 與 filter 的特殊組合,適合可能轉換失敗的工作。
let inputs = ["1", "2", "三", "4"]
let numbers = inputs.compactMap { Int($0) } // [1, 2, 4]
Int(“三”) 會得到 nil,compactMap 會將它篩掉。若使用 map { Int($0) },就會得到 [Int?],每個使用位置都得重複處理 Optional。「在邊界拆開 Optional」這項 Optional 篇的原則,轉成集合版本就是 compactMap。
flatMap:元素 → 陣列。展開一層巢狀結構。 當一個元素會展開成多個元素時使用。若像 sentences.flatMap { $0.split(separator: " ") } 一樣從句子陣列建立單字陣列,map 會產生 [[단어]] 這種巢狀結果,而 flatMap 會展開一層。與 compactMap 的區別可以這樣記:閉包回傳 Optional 就用 compactMap,回傳陣列(序列)就用 flatMap。
reduce:整個集合 → 單一值。 求總和、找最大值、建立字典等,所有折疊成單一結果的工作都屬於它。reduce(0, +) 是求總和;第一個引數是初始值,第二個是「將目前結果與下一個元素合併的方法」。只要記住一個效能技巧:累加陣列或字典時使用 reduce(into:)。一般 reduce 每一步都會複製累加值,而 into 版本會持續修改同一個值,大量資料時差異很大。
for 迴圈 vs 高階函式——可讀性的實際含義
「不用 for 迴圈、改用高階函式才像 Swift」這句話一半正確,一半危險。我們來訂出判準。
高階函式真正的優點不是短,而是意圖被宣告出來。看到 filter 的瞬間,讀者從簽章層級就能確定「這是在篩選,元素不會改變」。for 迴圈則必須讀完整個主體才能得出結論。這就是第 1 篇談到的表達力:讓意圖原樣呈現在程式碼中。
但只有每個步驟都很單純時,這項優點才成立。若閉包裡塞入三層條件判斷,還混入副作用(修改外部變數、print),「filter 就是在篩選」的預期便會落空,反而比 for 更難讀,違反最小驚訝原則。判準整理如下。
- 轉換、篩選、彙總的單純組合 → 高階函式。
- 每個步驟都需要複雜分支、副作用或中途跳出(break)→ for 迴圈。高階函式沒有合適的 break 對應工具;要在第一個符合條件的元素停止,可用
first(where:),但更複雜的提早結束邏輯仍以迴圈自然。 - 需要索引的工作 → 也可以使用搭配
enumerated()的高階函式,但複雜後就改用迴圈。
有一個反模式要明確說明:在 forEach 中對外部陣列 append。var result: [Int] = []; items.forEach { result.append($0 * 2) } 是手動重做 map 一行就能完成的事,只增加可變狀態。forEach 適合對每個元素產生副作用,例如發送通知;如果目的是產生某個結果,就該使用 map 系列。
鏈式呼叫的成本——中間陣列這位隱形訪客
串接高階函式看似優雅,但要知道每個步驟都會重新建立陣列。
let result = products
.filter { $0.inStock } // 中間陣列 1
.map { $0.price } // 中間陣列 2
.prefix(5) // 最終
如果有 100 萬個元素,filter 最多會建立一個 100 萬個元素的陣列,map 又會建立另一個。明明只需要前 5 個,卻仍完整走訪並配置兩次。
lazy 是這個問題的標準解法。像 products.lazy.filter{...}.map{...}.prefix(5) 一樣加入 lazy,就會改變管線的性質。各步驟不再立即執行並建立陣列,而是建立只記錄「要做什麼」的延遲序列;到最終消費時,元素才會逐一通過整條管線。填滿五個元素的瞬間就停止走訪,因此只會處理 100 萬個元素前方所需的部分,也不會有中間陣列。
但不是任何時候都該用 lazy。lazy 不儲存結果,因此同一個延遲序列消費兩次,就會計算兩次。閉包以參考方式儲存、稍後執行,也可能產生與 escaping 相關的限制。判準很簡單:資料量大且只消費部分資料(prefix、first(where:) 的組合)時,lazy 才能發揮價值;若會全部消費並儲存成陣列,通常直接執行較好。這其實就是屬性便利的 lazy var 背後那個問題的集合版本:「延後計算是否有好處?」
實戰組合配方
以下整理幾個常用組合,當作配方備用。
建立字典。 使用 Dictionary(uniqueKeysWithValues: users.map { ($0.id, $0) }) 建立 ID 查詢表。若可能有重複鍵,Dictionary(grouping: orders, by: { $0.customerID }) 是分組版本。
安全通過 Optional。 將伺服器回應中的字串 ID 陣列轉成 URL 陣列:ids.compactMap { URL(string: $0) }。失敗的項目會靜默篩除,型別確定為 [URL]。
排序組合。 除了 items.sorted { $0.priority > $1.priority },也建議知道使用 KeyPath 的 items.sorted(using: KeyPathComparator(\.priority, order: .reverse))。KeyPath 之後會另行介紹。
彙總。 購物車總額是 cart.reduce(0) { $0 + $1.price * Double($1.quantity) }。不過單純總和很多時候拆成 cart.map(\.subtotal).reduce(0, +) 會更好讀。reduce 閉包變複雜,就是該拆開的訊號。
總結
- 區別標準是閉包回傳什麼:新元素用 map、Bool 用 filter、Optional 用 compactMap、陣列用 flatMap、整體折疊成一個值用 reduce(累加集合用 reduce(into:))。
- 高階函式的優點是宣告意圖。閉包變複雜或混入副作用後,這項優點就會消失,應改回 for 迴圈。
- 鏈式呼叫每一步都會建立中間陣列。大型資料只消費部分內容時,用 lazy 建立延遲管線;全部消費時,維持立即執行。
- 如果你正用 forEach 建立陣列,那就是該改用 map 系列的地方。
下一篇是所有與 JSON 奮戰的 iOS 開發者都會遇到的主題:Codable 進階。內容涵蓋 CodingKeys、巢狀結構、日期策略,以及「因為一個欄位導致整體解碼失敗」問題的解法。

![[Swift 中級 #5] 高階函式實戰整理 封面圖](/assets/images/posts/ce6e6844-64dc-4e78-a2fc-3b8247084509/swift-higher-order-functions-1.jpg)