用 Result Builder 在 Swift 中写 DSL
Write a DSL in Swift using result builders
2021年6月10日
一句话判断
@resultBuilder(之前的 @functionBuilder)是 SwiftUI 的 ViewBuilder 背后的魔法——理解它之后你就能创建自己的 DSL,把复杂的数据构建逻辑隐藏在优雅的语法背后。
这场 Session 讲了什么
Session 深入讲解了 Swift 5.4 中正式引入的 @resultBuilder 特性。Result Builder 是一种编译器转换机制:你在一个标记了 @resultBuilder 的结构体中定义如何”累积”多个表达式,编译器会自动把你闭包中的多个语句转换成对累积方法的调用。
SwiftUI 的 @ViewBuilder 就是最典型的 result builder——你在 VStack 的闭包中写多个视图,编译器把它们转换成 buildBlock(view1, view2, view3) 的调用。
Session 用一个”图表构建 DSL”的例子,从零开始构建一个 result builder。覆盖了所有 builder 方法:buildBlock、buildOptional、buildEither、buildArray、buildExpression、buildFinalResult。
值得深挖的点
Result Builder 不是语法糖
它是一种编译器插件。当你写 @MyBuilder func makeThings() -> [Thing] 时,闭包中的每个表达式都被编译器改写。x 变成 buildExpression(x),if/else 变成 buildEither(first: ...) 或 buildEither(second: ...),可选值变成 buildOptional(...)。这意味着你可以完全控制表达式的求值方式——你甚至可以在 buildExpression 中做类型转换、验证或收集错误。
错误收集模式
Result Builder 的一个巧妙用法是用 buildExpression 和 buildBlock 收集所有错误而不是遇到第一个错误就停止。buildExpression 可以返回 Result<T, Error> 类型,buildBlock 可以累积所有错误到一个数组。这在构建配置文件解析器或表单验证 DSL 时非常有用——用户一次看到所有错误,而不是修一个再看下一个。
代码片段
构建一个图表 DSL
// 定义 Result Builder
@resultBuilder
struct ChartBuilder {
// 处理单个表达式
static func buildExpression(_ element: ChartElement) -> [ChartElement] {
[element]
}
// 处理多个表达式的组合
static func buildBlock(_ components: [ChartElement]...) -> [ChartElement] {
components.flatMap { $0 }
}
// 处理 if 语句
static func buildOptional(_ component: [ChartElement]?) -> [ChartElement] {
component ?? []
}
// 处理 if/else 语句
static func buildEither(first component: [ChartElement]) -> [ChartElement] {
component
}
static func buildEither(second component: [ChartElement]) -> [ChartElement] {
component
}
}
// 使用 DSL
@ChartBuilder
func makeChart(showLegend: Bool) -> [ChartElement] {
Bar(title: "一月", value: 42)
Bar(title: "二月", value: 58)
Line(points: [(0, 42), (1, 58)])
if showLegend {
Legend(position: .bottom)
}
}
带错误收集的验证 DSL
@resultBuilder
struct ValidationBuilder {
static func buildExpression(_ rule: ValidationRule) -> [ValidationError] {
rule.validate() // 返回错误数组,可能为空
}
static func buildBlock(_ errors: [ValidationError]...) -> [ValidationError] {
errors.flatMap { $0 }
}
static func buildFinalResult(_ errors: [ValidationError]) -> ValidationResult {
errors.isEmpty ? .valid : .invalid(errors)
}
}
// 使用
@ValidationBuilder
func validateForm(name: String, email: String, age: Int) -> ValidationResult {
NotEmpty(name, field: "姓名")
IsValidEmail(email, field: "邮箱")
GreaterThan(age, minimum: 18, field: "年龄")
}
// 一次返回所有验证错误
支持循环的 Result Builder
@resultBuilder
struct HTMLBuilder {
static func buildExpression(_ text: String) -> String { text }
static func buildExpression(_ node: HTMLNode) -> String { node.render() }
static func buildBlock(_ components: String...) -> String {
components.joined()
}
// 支持 for 循环
static func buildArray(_ components: [String]) -> String {
components.joined()
}
}
// 使用
@HTMLBuilder
func makeList(items: [String]) -> String {
"<ul>"
for item in items {
"<li>\(item)</li>"
}
"</ul>"
}
最佳实践
何时用 Result Builder: 当你有一个”累积多个表达式”的模式时。典型场景:UI 声明(HTML/SwiftUI)、配置文件定义、验证规则组合、测试断言组合。不要为了炫技而用——如果简单的数组字面量就能解决问题,不需要 builder。
API 设计: builder 方法应该处理所有需要的类型转换(通过 buildExpression 的重载)。对外暴露的 API 应该只有一个闭包参数标记了 @YourBuilder,其他参数提供上下文。
还有什么值得关注
- Swift 5.5 的
Regexbuilder 也是基于 result builder 实现的。 buildPartialBlock是 Swift 5.6 引入的新方法,让 builder 支持可变参数不需要为每个数量写重载。- Result Builder 可以和
async结合——buildExpression可以处理 async 表达式。