iOS 工程

宣告式 vs 命令式:用 SwiftUI 理解

SwiftUI 介紹文件的第一行,總會出現「宣告式(declarative)框架」這個說法。React 和 Jetpack Compose 也都稱自己為宣告式。

閱讀 3 分鐘
宣告式 vs 命令式:用 SwiftUI 理解 封面圖

SwiftUI 介紹文件的第一行,總會出現「宣告式(declarative)框架」這個說法。React 和 Jetpack Compose 也都稱自己為宣告式。

但真的被問到「什麼是宣告式?」時,往往很難回答。「就是程式碼很乾淨」並不是正確答案。

宣告式和命令式的差異,可以用一句話說清楚。你寫的是「如何(How)」,還是「什麼(What)」?

本文以日常範例掌握這個判準,再用 UIKit 和 SwiftUI 程式碼確認,並說明宣告式並非沒有代價。

以下是重點總結。

  1. 命令式:逐步指示到達想要結果的程序 — How
  2. 宣告式:描述想要的結果,將程序交給系統 — What
  3. SQL、HTML、map/filter 和 SwiftUI 都是宣告式的代表案例
  4. 宣告式 UI 的本質:宣告「狀態是這樣時,畫面就是這樣」,狀態變化後的更新則由框架處理

用搭計程車來比喻

命令式就像直接告訴司機路線:「右轉、直走 300 公尺、在號誌處左轉……」逐一指示每個步驟,這些步驟的總和便產生抵達目的地的結果。

宣告式則是說:「請到江南站。」只說目的地(What),路線選擇(How)交給司機和導航。

兩者都能抵達目的地。差別在於是由我擁有程序,還是只擁有結果的描述


在程式碼中,同一件事可以用兩種方式撰寫

來看看如何用兩種方式撰寫只挑出偶數並平方的程式碼。

// 命令式:我指示如何走訪,以及要存放在哪裡
var result: [Int] = []
for n in numbers {
    if n % 2 == 0 {
        result.append(n * n)
    }
}

// 宣告式:只描述我想要什麼
let result = numbers.filter { $0 % 2 == 0 }.map { $0 * $0 }

命令式版本會暴露迴圈變數、中間陣列、順序等「程序的零件」。宣告式版本只留下「篩選偶數後平方」這個意圖。走訪方式隱藏在 filter 和 map 裡。

你應該也已經在使用更熟悉的宣告式形式。SQL 只寫「給我符合這些條件的資料列」,不寫索引搜尋方式;HTML 只宣告結構「這裡是標題、這裡是段落」,不寫繪製程序。


來到 UI,差異會急遽放大

命令式 UI(UIKit)會撰寫變更畫面的程序

// UIKit: 每次狀態變更時,直接指示畫面操作
func updateBadge(count: Int) {
    if count > 0 {
        badgeLabel.isHidden = false
        badgeLabel.text = "\(count)"
    } else {
        badgeLabel.isHidden = true
    }
}

這種方式的難處是,開發者必須持續追蹤「目前畫面處於什麼狀態」。更新路徑一多,就很容易漏掉其中一條,形成「資料變了但畫面沒變」的錯誤。

宣告式 UI(SwiftUI)會宣告畫面在某個狀態下的樣子

// SwiftUI: count宣告狀態是這樣時,畫面就是這樣
struct BadgeView: View {
    let count: Int
    var body: some View {
        if count > 0 {
            Text("\(count)").badgeStyle()
        }
    }
}

count 改變時,不需要撰寫如何修正畫面。SwiftUI 會比較前一次與新的宣告,只更新必要的部分。這就把 UI 變成「狀態的函式」。既然更新程序的所有權交給框架,就沒有可能遺漏的更新程式碼。

命令式由我撰寫更新程序,宣告式則由框架計算差異
命令式由我撰寫更新程序,宣告式則由框架計算差異

宣告式並非沒有代價

為了保持平衡,也必須看看另一面。

隱藏程序的代價是,需要程序時會令人受挫。像「將捲動精確移到這個偏移量」這類命令式需求,在宣告式框架中反而需要繞路處理。

除錯的方式也不同。命令式可以沿著程序追蹤,但宣告式必須追查框架的判斷,例如「為什麼又重新繪製了?」不能完全不理解隱藏的 How。

效能調校最後仍會回到理解內部運作。SwiftUI 中檢視畫面被過度重新計算時,必須了解 diffing 與相依性追蹤的運作方式才能解決。

因此,正確的觀點不是「宣告式比較優越」,而是抽象層級的選擇。交出程序的所有權後,程式碼會以意圖為中心而變得簡潔,但細緻控制與理解內部運作便成了課題。

交出程序所有權、換取細緻控制權的取捨
交出程序所有權、換取細緻控制權的取捨

總結

  • 命令式指示抵達結果的程序(How),宣告式描述想要的結果(What)
  • SQL、HTML、map/filter 都是已經熟悉的宣告式形式
  • 命令式 UI 撰寫變更畫面的程序,因此容易出現狀態與畫面不一致的錯誤
  • 宣告式 UI 宣告「狀態是這樣時,畫面就是這樣」,更新則交給框架處理(UI = 狀態的函式)
  • 宣告式的代價:難以細緻控制程序,最後仍需要理解框架的內部運作
  • 本質不是優劣,而是選擇由誰擁有程序