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 调用的瞬间

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


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

有时我们想在代码块中使用 iffor,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 为什么采用这样的形式。

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

延伸阅读