Swift 与 Objective-C

Swift resultBuilder 完全掌握:Builder 模式为何演进为语言功能

第一次看到 SwiftUI 时,你是否也有过这样的想法?

4 分钟阅读
Swift resultBuilder 完全掌握:Builder 模式为何演进为语言功能 封面图

Swift resultBuilder 完全掌握:Builder 模式演进为语言语法的过程

第一次看到 SwiftUI 时,你是否也有过这样的想法?

“等等,只是在 VStack { Text("A"); Text("B") } 大括号中列出多个视图,怎么就能运行?”

第一次看到时确实像魔法。没有分号、逗号,也没有 return,却能自动完成组装。

这种魔法的真面目,就是今天要介绍的 Swift resultBuilder。

先说结论,resultBuilder 把我们过去作为设计模式手动实现的 Builder 模式提升为语言级功能,让编译器代替我们编写。它用一个大括号代码块语法,替代逐步组装对象的重复代码。


Builder 模式到底是什么?

先简单回顾一下传统的 Builder 模式。

Builder 模式不是一次性创建复杂对象,而是逐个添加组件来完成对象。

let request = URLRequestBuilder()
    .setURL("https://naver.com")
    .setMethod("GET")
    .addHeader("Accept", "application/json")
    .build()

我们会像这样通过链式调用方法进行组装。

虽然易于阅读,但也有缺点。每次都要手动创建 Builder 类,而且如果忘记 build(),对象就无法完成。

也就是说,开发者必须直接管理组装逻辑。

resultBuilder 正是把这部分工作交给编译器。


resultBuilder 是如何工作的?

核心思路很简单。

编译器会将大括号代码块中列出的值,依次传入预定义的规则函数,最终合并成一个结果。

这些规则函数就是类似 buildBlock 的静态方法。

例如,我们来创建一个收集字符串的简单 builder。

@resultBuilder
struct StringBuilder {
    static func buildBlock(_ parts: String...) -> String {
        parts.joined(separator: " ")
    }
}

@StringBuilder
func greeting() -> String {
    "你好"
    "resultBuilder"
    "是我"
}
// 结果:“你好 resultBuilder 是我"

看看 greeting() 函数内部,我们只是把字符串列成了三行,对吧?

编译器把这三行替换成了 buildBlock("안녕하세요", "resultBuilder", "입니다") 调用。

代码块列举转换为 buildBlock 调用的瞬间
代码块列举转换为 buildBlock 调用的瞬间

也就是说,原本需要手写的组装代码,现在由语法代替我们完成了。


条件语句和循环如何处理?

有时我们想在代码块中使用 if 或 for,SwiftUI 中也经常如此。

为此,resultBuilder 提供了额外的规则方法。

下面用表格整理主要方法。

方法 何时调用
buildBlock 将代码块中的多个值合并为一个值时
buildOptional 只有 if 而没有 else 时
buildEither(first:) / buildEither(second:) 处理 if-else 分支时
buildArray 收集 for 的循环结果时
buildExpression 先转换每一行的表达式时

实现哪些方法,决定了 builder 中允许使用哪些语法。

如果不创建 buildOptional,就无法在代码块中使用 if。

因此,SwiftUI 的 @ViewBuilder 几乎实现了所有这些方法。这样一来,就能在视图代码块中自然使用条件语句和循环。

@ViewBuilder 几乎实现了所有规则方法,因此才能做到这一点
@ViewBuilder 几乎实现了所有规则方法,因此才能做到这一点

实际使用后的优点与注意事项

我曾用 resultBuilder 创建小型 HTML 生成器和测试数据。

我最喜欢的一点是,调用位置变得声明式且简洁。由于组装过程被隐藏,读者可以专注于“要创建什么”。

不过,需要注意的地方也很明确。

  1. 编译错误消息经常不够友好。如果代码块中的类型不匹配,错误可能会指向完全无关的位置。
  2. 滥用反而会带来问题。如果只是创建一个简单数组,直接使用数组字面量更好。
  3. 调试时,很难追踪实际调用了哪个 build... 方法。

因此,我建议只在 需要反复组装的结构中使用它,例如 DSL(Domain-Specific Language,领域特定语言)。SwiftUI、服务器路由定义和查询 builder 都是例子。

给 iOS 开发实践的一个建议是:通常先理解并熟练使用 @ViewBuilder,比亲自创建 @resultBuilder 更重要。

不要只用眼睛阅读,今天一定要亲手输入一次这个示例
不要只用眼睛阅读,今天一定要亲手输入一次这个示例

总结

总结来说,resultBuilder 是语言吸收我们过去手写的 Builder 模式后形成的进化版本。

只要掌握“列在大括号中,编译器就会负责组装”的感觉,就能自然理解 SwiftUI 为什么采用这样的形式。

请亲自输入一遍今天的示例代码。相比只用眼睛阅读,这样能更快掌握。

延伸阅读