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
    }
}

这种方式的难点在于,开发者必须持续跟踪“当前界面处于什么状态”。如果更新路径很多,就很容易漏掉一条,导致“数据变了但界面没变”的典型 bug。

声明式 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 编写改变界面的过程,容易出现状态与界面不一致的 bug
  • 声明式 UI 声明“状态是这样时,界面就是这样”,更新由框架负责(UI = 状态的函数)
  • 声明式的代价:难以进行细粒度的过程控制,最终仍需理解框架的内部运行机制
  • 本质不是优劣,而是选择由谁拥有过程