SwiftUI 是一個(gè)聲明式的 UI 開發(fā)方式淘这。在能夠進(jìn)一步之前穴张,我們最好先弄清指令式和聲明式兩種編程方式的區(qū)別掉伏,從而更好地理解SwiftUI的強(qiáng)大與精妙缝呕。
指令式編程
C,C++ 和大部分的更早期的語(yǔ)言都遵從指令式編程的范式岖免。一般來(lái)說(shuō)岳颇,指令式編程支持三種語(yǔ)句:
運(yùn)算語(yǔ)句:將某個(gè)值保存到變量中,計(jì)算某個(gè)表達(dá)式的結(jié)果颅湘,或者方法調(diào)用话侧。比如 let a = 1 + 2 就是一個(gè)標(biāo)準(zhǔn)的運(yùn)算語(yǔ)句。
循環(huán)語(yǔ)句:在特定條件下反復(fù)運(yùn)行某些語(yǔ)句闯参,比如 for 和 while瞻鹏。
條件語(yǔ)句:如果某些條件成立時(shí)才運(yùn)行某個(gè)區(qū)塊的代碼悲立,否則就將省去。比如 if 和 switch 都是條件語(yǔ)句新博⌒较Γ”
通過(guò)組合這些語(yǔ)句,我們?cè)谥噶钍骄幊讨兄饤l指示計(jì)算機(jī)如何工作赫悄。早期的指令式編程語(yǔ)言都是針對(duì)計(jì)算機(jī)本身的機(jī)器或匯編語(yǔ)言原献。在這些語(yǔ)言中,指令的設(shè)計(jì)貼近計(jì)算機(jī)的運(yùn)算硬件設(shè)計(jì)埂淮,這讓硬件的運(yùn)行更容易和高效姑隅。在隨后的早期高級(jí)語(yǔ)言中,對(duì)這些指令語(yǔ)句的映射往往成為了設(shè)計(jì)語(yǔ)言的主流方向倔撞。雖然這有利于編譯器的編寫和最終的運(yùn)行效率讲仰,但同時(shí)也阻礙了復(fù)雜程序的設(shè)計(jì)。
舉個(gè)簡(jiǎn)單的例子痪蝇,比如我們有一個(gè)學(xué)生系統(tǒng)鄙陡,記錄了學(xué)生姓名,并用一個(gè)字典記錄了各科目的考試成績(jī):
struct Student {
let name: String
let scores: [科目: Int]
}
enum 科目: String, CaseIterable {
case 語(yǔ)文, 數(shù)學(xué), 英語(yǔ), 物理
}
假設(shè)我們有一些學(xué)生的數(shù)據(jù):
let s1 = Student(
name: "Jane",
scores: [.語(yǔ)文: 86, .數(shù)學(xué): 92, .英語(yǔ): 73, .物理: 88]
)
let s2 = Student(
name: "Tom",
scores: [.語(yǔ)文: 99, .數(shù)學(xué): 52, .英語(yǔ): 97, .物理: 36]
)
let s3 = Student(
name: "Emma",
scores: [.語(yǔ)文: 91, .數(shù)學(xué): 92, .英語(yǔ): 100, .物理: 99]
)
let students = [s1,s2,s3]
我們現(xiàn)在想要檢查 students 里的學(xué)生的平均分躏啰,并輸出第一名的姓名趁矾。使用指令式的方式,依靠運(yùn)算丙唧,循環(huán)和條件語(yǔ)句愈魏,可以給出下面這種解決方案:
var best: (Student, Double)?
for s in students {
var totalScore = 0
for key in 科目.allCases {
totalScore += s.scores[key] ?? 0
totalScore += s.scores[key] ?? 0
}
let averageScore = Double(totalScore) / Double(科目.allCases.count)
if let temp = best {
if averageScore > temp.1 {
best = (s, averageScore)
}
} else {
best = (s, averageScore)
}
}
if let best = best {
print("最高平均分: \(best.1), 姓名: \(best.0.name)")
} else {
print("students 為空")
}
如果第一次讀這段代碼的話,想要了解它到底做了什么或者到底最后會(huì)得到怎樣的結(jié)果想际,可能必須要仔細(xì)閱讀并理解每一行指令培漏。代碼行數(shù)與 bug 多少往往是正相關(guān),而這種開發(fā)方式也會(huì)為代碼維護(hù)帶來(lái)巨大的挑戰(zhàn)胡本。
我們有什么辦法可以減輕開發(fā)者的負(fù)擔(dān)牌柄,讓計(jì)算機(jī)更加“智能”地為我們解決問(wèn)題呢?
聲明式編程
聲明式的編程范式正好站在指令式的對(duì)面:如果說(shuō)指令式是教會(huì)計(jì)算機(jī)“怎么做”侧甫,那么聲明式就是教會(huì)計(jì)算機(jī)“做什么”珊佣。指令式編程是描述過(guò)程,期望程序執(zhí)行以得到我們想要的結(jié)果披粟;而聲明式編程則是描述結(jié)果咒锻,讓計(jì)算機(jī)為我們考慮和組織出具體過(guò)程,最后得到被描述的結(jié)果守屉。
現(xiàn)代語(yǔ)言中惑艇,一般使用函數(shù)式編程或者 DSL 的方式來(lái)實(shí)現(xiàn)聲明式的編程方式。
對(duì)于上面的例子,使用函數(shù)式編程的方式滨巴,可以將它改寫為:
func average(_ scores: [科目: Int]) -> Double {
return Double(scores.values.reduce(0, +)) /
Double(科目.allCases.count)
}
let bestStudent = students
.map { ($0, average($0.scores)) }
.sorted { $0.1 > $1.1 }
.first
在這段代碼中思灌,我們首先將 students 映射為了 (Student, 平均分) 的數(shù)組,然后對(duì)平均分按降序進(jìn)行排序恭取,最后取出排序后的首個(gè)元素泰偿。在這個(gè)過(guò)程中,我們僅僅是用語(yǔ)句描述了我們想要的結(jié)果蜈垮,例如:按規(guī)則進(jìn)行映射耗跛、對(duì)元素進(jìn)我們想要的結(jié)果,例如:按規(guī)則進(jìn)行映射攒发、對(duì)元素進(jìn)行排序等课兄。我們并不關(guān)心代碼在底層具體是如何操作數(shù)組的,而只關(guān)心這段代碼能夠得到我們所描述的結(jié)果晨继。
另一種經(jīng)常用來(lái)實(shí)現(xiàn)聲明式編程的方法是領(lǐng)域特定語(yǔ)言 (DSL),其中一個(gè)典型的代表是 SQL搬俊。SQL 被用在關(guān)系數(shù)據(jù)庫(kù)中紊扬,專門用在結(jié)構(gòu)化查詢這一特定領(lǐng)域,它通過(guò)描述期望的結(jié)果來(lái)對(duì)數(shù)據(jù)庫(kù)進(jìn)行查詢唉擂。上面的例子在 SQL 中的對(duì)應(yīng)語(yǔ)句如下:
select name, avg(score) as avg_score
from scores group by name order by avg_score;
不論是使用函數(shù)式的方式餐屎,還是使用 DSL 的方式,我們都能夠比較輕松地閱讀代碼玩祟,更快速地理解代碼的意圖腹缩。指令式編程更偏向于是“寫給計(jì)算機(jī)的語(yǔ)言”,而相對(duì)地空扎,聲明式編程則更偏向于“寫給人看的語(yǔ)言”藏鹊。將具體的步驟和工作交給底層,同時(shí)也最大限度避免了由于開發(fā)者的錯(cuò)誤而造成的 bug转锈。
聲明式的UI
使用聲明式的編程方式來(lái)進(jìn)行用戶界面開發(fā)盘寡,在近年來(lái)是頗為熱門和受到歡迎的實(shí)踐方式。當(dāng)前流行的聲明式 UI 的思想撮慨,可以追溯到 Elm 語(yǔ)言的設(shè)計(jì)竿痰。在之后,React 的 Component 和 Flutter 的 Widget 都繼承了這種思想砌溺,這類聲明式 UI 都有如下特點(diǎn):
代表 UI 層的 View 并不是真實(shí)負(fù)責(zé)渲染的傳統(tǒng)意義的視圖層級(jí)影涉,而是一個(gè)“虛擬的”對(duì) View 組織關(guān)系的描述 (聲明)。
決定 UI 的用戶狀態(tài) State 被存儲(chǔ)在某個(gè)或某幾個(gè)對(duì)象中规伐。
用一個(gè)函數(shù)描述 View蟹倾,這個(gè)函數(shù)的輸入?yún)?shù)是 State,即 View = f(State)楷力。
框架在 State 改變時(shí)喊式,調(diào)用上述函數(shù)獲取對(duì)應(yīng)新的 State 的 View孵户,并與當(dāng)前的 View 進(jìn)行差分計(jì)算,并重新渲染更改的部分岔留。
一般來(lái)說(shuō)夏哭,View = f(State) 中的函數(shù) f 是純函數(shù),也就是對(duì)于某個(gè)特定的輸入 State献联,所對(duì)應(yīng)的 View 是確定的竖配,不隨其他變量而改變。我們可以單純地通過(guò)控制和改變 State 來(lái)得到確定的 UI里逆,這是使用聲明式的方法來(lái)構(gòu)建 UI 的基礎(chǔ)进胯。