一句话总结
@resultBuilder
是一套编译期 DSL 引擎:通过在 Builder 类型里实现若干静态方法,把"多行表达式"在编译期重写成"单个结果值"。
SwiftUI 的 @ViewBuilder
只是其中一个特例,你可以用它写 SQL、验证规则、网络请求、甚至是剧本脚本。
概念总览:什么是 resultBuilder?
要素 | 说明 |
---|---|
官方定义 | 一个函数构建器(function builder)的语法糖,编译器在编译期把闭包内的多条语句转成一个返回值。 |
核心思想 | 把语法树 → 值。 |
必备协议 | 无正式 protocol,但编译器会查找 Builder 类型里的一组静态方法(见下表)。 |
触发方式 | 在形参、变量、计算属性前写 @YourBuilder 。 |
编译器查找的静态方法清单
完整版清单可以参阅 doc.swiftgg.team/documentati...
方法 | 何时被调用 | 典型实现 | 备注 |
---|---|---|---|
buildBlock(_:) |
闭包内有多条语句 | 把语句数组拼接成单一值 | 必须实现 |
buildExpression(_:) |
每条语句求值后 | 把单个语句转成中间表示 | 可选,默认 identity |
buildOptional(_:) |
可选链 if let / ? |
处理 nil | 可选 |
buildEither(first:) / buildEither(second:) |
if/else 或 switch |
返回同一类型,保证分支同质 | 可选 |
buildArray(_:) |
for-in 循环 | 合并数组 | 可选 |
buildFinalResult(_:) |
最终返回前 | 二次加工 | 可选 |
SwiftUI 视角:@ViewBuilder 内幕
最小可运行示例
swift
// 伪代码:VStack 的简化签名
struct VStack<Content: View>: View {
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
}
swift
// 用户写的 DSL
VStack {
Text("A")
Text("B")
}
编译器在后台把闭包重写成:
swift
VStack {
ViewBuilder.buildBlock(
Text("A"),
Text("B")
)
}
some View
vs @ViewBuilder
写法 | 返回类型 | 允许语句数 |
---|---|---|
var body: some View { Text("") } |
一个具体类型 Text |
只能 1 条 |
@ViewBuilder var body: some View { Text(""); Text("") } |
_TupleView<(Text,Text)> |
支持多条 |
离开 SwiftUI:DSL 的 3 个通用范式
范式 A:收集器(Collector)
把多条语句收集成数组 / 字符串。 示例:SQL 查询构建器
swift
@resultBuilder
struct SQLQueryBuilder {
static func buildBlock(_ parts: String...) -> String {
parts.joined(separator: " ")
}
static func buildExpression(_ part: String) -> String { part }
static func buildOptional(_ part: String?) -> String { part ?? "" }
static func buildEither(first: String) -> String { first }
static func buildEither(second: String) -> String { second }
}
@SQLQueryBuilder
func makeQuery(@SQLQueryBuilder _ builder: () -> String) -> String {
builder()
}
let isAdmin = false
let sql = makeQuery {
"SELECT id, name"
"FROM users"
if isAdmin {
"WHERE role = 'admin'"
} else {
"WHERE active = 1"
}
}
print(sql)
// SELECT id, name FROM users WHERE active = 1
范式 B:验证器(Validator)
把多条规则组合成"先组合再执行"的验证管道。 完整代码(含错误类型)
swift
enum ValidationError: Error, LocalizedError {
case empty, invalidEmail, tooShort
var errorDescription: String? { ... }
}
protocol Rule {
func validate(_ value: String) throws
}
struct NotEmpty: Rule {
func validate(_ v: String) throws { if v.isEmpty { throw ValidationError.empty } }
}
struct Email: Rule {
func validate(_ v: String) throws {
// 简单正则
let regex = #"^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"#
if !NSPredicate(format: "SELF MATCHES %@", regex).evaluate(with: v) {
throw ValidationError.invalidEmail
}
}
}
struct MinLength: Rule {
let min: Int
func validate(_ v: String) throws {
if v.count < min { throw ValidationError.tooShort }
}
}
@resultBuilder
struct ValidationBuilder {
static func buildBlock(_ components: Rule...) -> [Rule] { components }
static func buildOptional(_ c: [Rule]?) -> [Rule] { c ?? [] }
static func buildEither(first: [Rule]) -> [Rule] { first }
static func buildEither(second: [Rule]) -> [Rule] { second }
}
struct Validator {
private let rules: [Rule]
init(@ValidationBuilder _ builder: () -> [Rule]) {
rules = builder()
}
func validate(_ value: String) throws {
try rules.forEach { try $0.validate(value) }
}
}
// 使用
let emailValidator = Validator {
NotEmpty()
Email()
}
try emailValidator.validate("test@example.com")
范式 C:容器构建器(Container)
把语句构建成树形结构,如 JSON、YAML、HTML。 示例:极简 HTML DSL
swift
// 1. 定义 HTMLNode
enum HTMLNode {
case text(String)
indirect case tag(String, [HTMLNode]) // 间接枚举支持嵌套
}
// 2. 实现更健壮的 HTMLBuilder
@resultBuilder
enum HTMLBuilder {
// 收集多个 HTMLNode 为一个数组(核心)
static func buildBlock(_ components: HTMLNode...) -> [HTMLNode] {
components
}
// 将字符串字面量转换为 .text 节点
static func buildExpression(_ expression: String) -> HTMLNode {
.text(expression)
}
static func buildExpression(_ component: HTMLNode) -> HTMLNode {
component
}
// 可选:支持空内容(如空闭包)
static func buildOptional(_ component: [HTMLNode]?) -> [HTMLNode] {
component ?? []
}
}
// 3. 定义 tag 函数(关键:明确闭包返回 [HTMLNode])
func tag(_ name: String, @HTMLBuilder children: () -> [HTMLNode]) -> HTMLNode {
.tag(name, children())
}
// 构建一个简单的 HTML 结构
let pageTitle = tag("h1") { "欢迎使用 HTML DSL" }
let content = tag("p") {
"这是 "
tag("strong") { "极简" }
" 示例"
}
let html = tag("html") {
tag("head") {
tag("title") { pageTitle }
}
tag("body") {
content
}
}
// 验证输出(打印结构)
func printNode(_ node: HTMLNode, indent: Int = 0) {
let prefix = String(repeating: " ", count: indent)
switch node {
case .text(let text):
print("\(prefix)Text: \(text)")
case .tag(let name, let children):
print("\(prefix)<\(name)>")
children.forEach { printNode($0, indent: indent + 2) }
print("\(prefix)</\(name)>")
}
}
printNode(html)
性能、并发与限制
维度 | 说明 |
---|---|
编译期 | 所有转换在编译期完成,零运行时开销 |
类型安全 | 若分支返回不同类型会直接编译失败 |
并发 | 在 SwiftUI 中,需遵守 @MainActor ;自定义 DSL 里无限制 |
调试 | 可在 buildBlock 打断点观察重写的结构 |
我的实践心得 & 脑洞扩展
网络请求 DSL
用 Builder 把链式调用改写成顺序写法:
swift
let req = Request {
BaseURL("https://api.xxx.com")
Path("/v1/user")
GET
Header(.authorization, "Bearer \(token)")
}
剧本 / 故事板
给游戏对话系统写 DSL:
swift
let scene = Story {
Speaker("Alice")
Say("Hello!")
Choice("Reply", next: .node(id: 2))
}
CI / CD 配置
在 Swift Package 里直接声明 .gitlab-ci.yml
的 Swift 版本:
swift
let pipeline = Pipeline {
Stage("test") {
Script("swift test")
}
}