深入理解 Swift `@resultBuilder`:从 SwiftUI 到通用 DSL 的完全指南

一句话总结

@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/elseswitch 返回同一类型,保证分支同质 可选
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")
    }
}
相关推荐
HarderCoder1 天前
深入理解 Swift 的 `withExtendedLifetime`:原理、场景与实战
swift
songgeb1 天前
DiffableDataSource in iOS
ios·swift
银二码1 天前
flutter踩坑插件:Swift架构不兼容
开发语言·flutter·swift
HarderCoder2 天前
深入理解 SwiftUI 中的 @ViewBuilder:从语法糖到实战
swift
HarderCoder2 天前
Swift 中的可调用类型:彻底搞懂 `callAsFunction`、`@dynamicCallable` 与 `@dynamicMemberLookup`
swift
CuiXg2 天前
iOS XML 处理利器:CNXMLParser 与 CNXMLDocument 深度解析
ios·swift
HarderCoder2 天前
Swift 中 Enum 与 Struct:如何为状态建模选择最合适的工具
swift
大熊猫侯佩2 天前
韦爵爷闯荡 Swift 6 江湖:单例秘籍新解(上)
swift·编程语言·apple
大熊猫侯佩2 天前
韦爵爷闯荡 Swift 6 江湖:单例秘籍新解(中)
swift·敏捷开发·apple