SwiftUI 是一个声明式的 UI 开发方式。在能够进一步之前,我们最好先弄清指令式和声明式两种编程方式的区别,从而更好地理解SwiftUI的强大与精妙。
指令式编程
C,C++ 和大部分的更早期的语言都遵从指令式编程的范式。一般来说,指令式编程支持三种语句:
运算语句:将某个值保存到变量中,计算某个表达式的结果,或者方法调用。比如 let a = 1 + 2 就是一个标准的运算语句。
循环语句:在特定条件下反复运行某些语句,比如 for 和 while。
条件语句:如果某些条件成立时才运行某个区块的代码,否则就将省去。比如 if 和 switch 都是条件语句。”
通过组合这些语句,我们在指令式编程中逐条指示计算机如何工作。早期的指令式编程语言都是针对计算机本身的机器或汇编语言。在这些语言中,指令的设计贴近计算机的运算硬件设计,这让硬件的运行更容易和高效。在随后的早期高级语言中,对这些指令语句的映射往往成为了设计语言的主流方向。虽然这有利于编译器的编写和最终的运行效率,但同时也阻碍了复杂程序的设计。
举个简单的例子,比如我们有一个学生系统,记录了学生姓名,并用一个字典记录了各科目的考试成绩:
struct Student {
let name: String
let scores: [科目: Int]
}
enum 科目: String, CaseIterable {
case 语文, 数学, 英语, 物理
}
假设我们有一些学生的数据:
let s1 = Student(
name: "Jane",
scores: [.语文: 86, .数学: 92, .英语: 73, .物理: 88]
)
let s2 = Student(
name: "Tom",
scores: [.语文: 99, .数学: 52, .英语: 97, .物理: 36]
)
let s3 = Student(
name: "Emma",
scores: [.语文: 91, .数学: 92, .英语: 100, .物理: 99]
)
let students = [s1,s2,s3]
我们现在想要检查 students 里的学生的平均分,并输出第一名的姓名。使用指令式的方式,依靠运算,循环和条件语句,可以给出下面这种解决方案:
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 为空")
}
如果第一次读这段代码的话,想要了解它到底做了什么或者到底最后会得到怎样的结果,可能必须要仔细阅读并理解每一行指令。代码行数与 bug 多少往往是正相关,而这种开发方式也会为代码维护带来巨大的挑战。
我们有什么办法可以减轻开发者的负担,让计算机更加“智能”地为我们解决问题呢?
声明式编程
声明式的编程范式正好站在指令式的对面:如果说指令式是教会计算机“怎么做”,那么声明式就是教会计算机“做什么”。指令式编程是描述过程,期望程序执行以得到我们想要的结果;而声明式编程则是描述结果,让计算机为我们考虑和组织出具体过程,最后得到被描述的结果。
现代语言中,一般使用函数式编程或者 DSL 的方式来实现声明式的编程方式。
对于上面的例子,使用函数式编程的方式,可以将它改写为:
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, 平均分) 的数组,然后对平均分按降序进行排序,最后取出排序后的首个元素。在这个过程中,我们仅仅是用语句描述了我们想要的结果,例如:按规则进行映射、对元素进我们想要的结果,例如:按规则进行映射、对元素进行排序等。我们并不关心代码在底层具体是如何操作数组的,而只关心这段代码能够得到我们所描述的结果。
另一种经常用来实现声明式编程的方法是领域特定语言 (DSL),其中一个典型的代表是 SQL。SQL 被用在关系数据库中,专门用在结构化查询这一特定领域,它通过描述期望的结果来对数据库进行查询。上面的例子在 SQL 中的对应语句如下:
select name, avg(score) as avg_score
from scores group by name order by avg_score;
不论是使用函数式的方式,还是使用 DSL 的方式,我们都能够比较轻松地阅读代码,更快速地理解代码的意图。指令式编程更偏向于是“写给计算机的语言”,而相对地,声明式编程则更偏向于“写给人看的语言”。将具体的步骤和工作交给底层,同时也最大限度避免了由于开发者的错误而造成的 bug。
声明式的UI
使用声明式的编程方式来进行用户界面开发,在近年来是颇为热门和受到欢迎的实践方式。当前流行的声明式 UI 的思想,可以追溯到 Elm 语言的设计。在之后,React 的 Component 和 Flutter 的 Widget 都继承了这种思想,这类声明式 UI 都有如下特点:
代表 UI 层的 View 并不是真实负责渲染的传统意义的视图层级,而是一个“虚拟的”对 View 组织关系的描述 (声明)。
决定 UI 的用户状态 State 被存储在某个或某几个对象中。
用一个函数描述 View,这个函数的输入参数是 State,即 View = f(State)。
框架在 State 改变时,调用上述函数获取对应新的 State 的 View,并与当前的 View 进行差分计算,并重新渲染更改的部分。
一般来说,View = f(State) 中的函数 f 是纯函数,也就是对于某个特定的输入 State,所对应的 View 是确定的,不随其他变量而改变。我们可以单纯地通过控制和改变 State 来得到确定的 UI,这是使用声明式的方法来构建 UI 的基础。