1 前言
结果生成器(以前叫做函数生成器)是swift5.4中引入的一项新feature,它是SwiftUI中支持ViewBuilder的技术。随着Xcode12.5的发布(目前处于beta测试阶段),苹果正式向开发者开放了它,允许我们为各种用例创建自己的自定义结果生成器。
本文讲讲解结果生成器的基本概念、工作原理以及如何使用它来创建自己的自定义结果生成器。
话不多说,让我们马上开始吧!
2 基本形式
作为演示,我们创建一个字符串生成器,并使用⭐️ 作为分隔符。例如,给定“Hello”和“World”,我们的字符串生成器将返回一个连接的字符串“Hello”⭐️“World”。
让我们开始使用结果生成器的最基本形式来构建字符串生成器:
- resultBuilder
- struct StringBuilder {
- static func buildBlock(_ components: String...) -> String {
- return components.joined(separator: "")
- }
- }
你可以通过使用@resultBuilder属性标记自定义结构体,并强制实现buildBlock(_:)静态方法来定义结果生成器。
buildBlock(_:)方法类似于StringBuilder的入口点,它接受组件的可变参数,这意味着它可以是1个或多个字符串。在buildBlock(_:)方法中,我们可以对给定的组件进行任何处理。在这个例子中,我们将使用 "⭐️"作为分隔符。
在实现buildBlock(_:)方法时,需要遵循一条规则:返回的数据类型必须与components数据类型匹配。以StringBuilder为例,buildBlock(_:)方法组件是String类型的,因此其返回类型也必须是String。
要创建StringBuilder实例,可以使用@StringBuilder标记函数或变量:
- // 用 `StringBuilder`标记函数
- @StringBuilder func buildStringFunc() -> String {
- // components区域
- // ...
- }
- // 用 `StringBuilder`标记变量
- @StringBuilder var buildStringVar: String {
- // components区域
- // ...
- }
注意上面提到的组件区域,它是向StringBuilder提供所需字符串的地方。components区域中的每一行表示buildBlock(_:)可变参数的一个组件。以下面的StringBuilder为例:
- @StringBuilder func greet() -> String {
- "Hello"
- "World"
- }
- print(greet())
- // Output: "HelloWorld"
可以翻译为:
- func greetTranslated() -> String {
- //解析StringBuilder中的所有部分组件`
- let finalOutput = StringBuilder.buildBlock("Hello", "World")
- return finalOutput
- }
- print(greetTranslated())
小Tip:您可以在buildBlock(_:)方法中添加print语句,以查看何时触发它以及在任何给定时间提供了哪些组件。
这就是创建结果生成器所需的全部内容。现在您已经看到了一个基本的结果生成器,让我们继续向StringBuilder添加更多的功能。
3 选择语句
没有“else”块的“if”语句
假设我们要扩展greet()方法的功能,接受name参数然后根据name来跟用户打招呼。我们可以这样更新greet()方法:
- @StringBuilder func greet(name: String) -> String {
- "Hello"
- "World"
- if !name.isEmpty {
- "to"
- name
- }
- }
- print(greet(name: "Swift Senpai"))
- // Expected output: "HelloWorldtoSwift Senpai"
这样修改以后,你应该会看到编译器开始抱怨:
- Closure containing control flow statement cannot be used with result builder 'StringBuilder'
- 包含控制流语句的闭包不能与结果生成器“StringBuilder”一起使用
这是因为我们的StringBuilder目前不理解什么是if语句。为了支持没有else的if语句,我们必须将以下结果构建方法添加到StringBuilder中。
- @resultBuilder
- struct StringBuilder {
- // ...
- // ...
- static func buildOptional(_ component: String?) -> String {
- return component ?? ""
- }
- }
它的工作原理是,当满足if语句条件时,把部分结果传递给buildOptional(_:)方法,否则把nil传递给buildOptional(_:)方法。
为了让你更清楚地了解结果生成器是如何解析覆盖下的每个部分组件,上面的greet(name:)函数等效于以下代码段:
- func greetTranslated(name: String) -> String {
- // Resolve all partial components within the `if` block
- var partialComponent1: String?
- if !name.isEmpty {
- partialComponent1 = StringBuilder.buildBlock("to", name)
- }
- // Resolve the entire `if` block
- let partialComponent2 = StringBuilder.buildOptional(partialComponent1)
- // Resolve all partial components in `StringBuilder`
- let finalOutput = StringBuilder.buildBlock("Hello", "World", partialComponent2)
- return finalOutput
- }
- print(greetTranslated(name: "Swift Senpai"))
- // Output: "HelloWorldtoSwift Senpai"
注意结果生成器是如何首先解析if块中的任何内容,然后递归地传递和解析部分组件,直到它获得最终输出的。此行为非常重要,因为它从根本上演示了结果生成器如何解析components区域中的所有组件。
小Tip:添加buildOptional(_:)方法不仅支持没有else块的if语句,还支持可选绑定。
此时,如果尝试使用空的name调用greet(name:)函数,将得到以下输出:
- print(greet(name: ""))
- // Actual output: HelloWorld
- // Expected output: HelloWorld
输出字符串的末尾额外的"⭐️",是由于buildBlock(_:)方法通过buildOptional(_:)方法连接空字符串返回。为了解决这个问题,我们可以简单地更新buildBlock(_:)方法,在连接之前过滤掉组件中的所有空字符串:
- static func buildBlock(_ components: String...) -> String {
- let filtered = components.filter { $0 != "" }
- return filtered.joined(separator: "")
- }
带有“else”块的“if”语句
我们的StringBuilder现在比以前更聪明了,但是说“Hello⭐️World⭐️to⭐️“Swift Senpai”听起来怪怪的。
让我们把它变得更聪明,当name不为空时它就可以输出"Hello⭐️to⭐️[name]",否则输出 "Hello⭐️World"。继续更新greet(name:)函数,如下所示:
- @StringBuilder func greet(name: String) -> String {
- "Hello"
- if !name.isEmpty {
- "to"
- name
- } else {
- "World"
- }
- }
- print(greet(name: "Swift Senpai"))
- // Expected output: "HellotoSwift Senpai"
您将再次看到编译错误:
- Closure containing control flow statement cannot be used with result builder 'StringBuilder'
- 包含控制流语句的闭包不能与结果生成器“StringBuilder”一起使用
这一次,由于额外的else块,我们必须实现另外两种结果构建方法:
- static func buildEither(first component: String) -> String {
- return component
- }
- static func buildEither(second component: String) -> String {
- return component
- }
这两种方法总是结合在一起的。当满足if块条件时,buildery(first:)方法将触发;然而,当满足else块条件时,buildery(second:)方法将触发。下面是一个等价函数,可以帮助您理解场景背后发生的逻辑:
- func greetTranslated(name: String) -> String {
- var partialComponent2: String!
- if !name.isEmpty {
- // Resolve all partial components within the `if` block
- let partialComponent1 = StringBuilder.buildBlock("to", name)
- // Resolve the entire `if-else` block
- partialComponent2 = StringBuilder.buildEither(first: partialComponent1)
- } else {
- // Resolve all partial components within the `else` block
- let partialComponent1 = StringBuilder.buildBlock("World")
- // Resolve the entire `if-else` block
- partialComponent2 = StringBuilder.buildEither(second: partialComponent1)
- }
- // Resolve all partial components in `StringBuilder`
- let finalOutput = StringBuilder.buildBlock("Hello", partialComponent2)
- return finalOutput
- }
- print(greetTranslated(name: "Swift Senpai"))
- // Output: "HellotoSwift Senpai"
4 for-in循环
接下来,让我们更新greet(name:)函数,在问候用户之前倒计时,因为为什么不呢?🤷🏻♂️
继续更新greet(name:)函数,如下所示:
- @StringBuilder func greet(name: String, countdown: Int) -> String {
- for i in (0...countdown).reversed() {
- "\(i)"
- }
- "Hello"
- if !name.isEmpty {
- "to"
- name
- } else {
- "World"
- }
- }
- print(greet(name: "Swift Senpai", countdown: 5))
- // Expected output: 543210HellotoSwift Senpai
注意,我在函数的开头添加了一个倒计时参数和for循环。for循环将执行从给定值到0的倒计时。
下一步也是最后一步是使用以下结果生成方法更新StringBuilder:
- static func buildArray(_ components: [String]) -> String {
- return components.joined(separator: "")
- }
请注意,buildArray(_:)方法与结果生成方法的其余部分稍有不同,它将数组作为输入。在场景后面发生的是,在每次迭代结束时,for循环将生成一个字符串(部分组件)。在经历了所有迭代之后,每个迭代的结果将被分组为一个数组,并将其传递给buildArray(_:)方法。为了更好地说明流程,下面是等效函数:
- func greetTranslated(name: String, countdown: Int) -> String {
- // Resolve partial components in each iteration
- var partialComponents = [String]()
- for i in (0...countdown).reversed() {
- let component = StringBuilder.buildBlock("\(i)")
- partialComponents.append(component)
- }
- // Resolve the entire `for-in` loop
- let loopComponent = StringBuilder.buildArray(partialComponents)
- // `if-else` block processing here
- // ...
- // ...
- // ...
- // Resolve all partial components in `StringBuilder`
- let finalOutput = StringBuilder.buildBlock(loopComponent, "Hello", partialComponent2)
- return finalOutput
- }
- print(greetTranslated(name: "Swift Senpai", countdown: 5))
- // Output: 543210HellotoSwift Senpai
有了它,我们的StringBuilder就能够处理for-in循环。现在试着运行代码,你会看到在Xcode控制台打印"543210⭐️Hello⭐️to⭐️Swift Senpai"。
注:
添加buildArray(_:)方法将不支持while 循环。实际上,for-in 循环是结果生成器支持的唯一循环方法。
5 支持不同的数据类型
在这个阶段,我们已经使StringBuilder非常灵活,它现在可以接受选择语句、for循环和可选绑定作为输入。但是,有一个很大的限制:它只能支持字符串作为输入和输出数据类型。
幸运的是,支持各种输入和输出数据类型非常简单。我来教你怎么做。
启用各种输入数据类型
假设我们想让StringBuilder支持Int作为输入类型,我们可以将以下结果构建方法添加到StringBuilder中:
- static func buildExpression(_ expression: Int) -> String {
- return "\(expression)"
- }
此buildExpression(_:)方法是可选的,它接受整型作为输入并返回字符串。一旦实现,它将成为结果生成器的入口点,并充当适配器,将其输入数据类型转换为buildBlock(:_)方法接受的数据类型。
这就是为什么您会看到多个“Cannot convert value of type'String'to expected argument type'Int'”错误出现在我们添加了buildExpression(:_)方法之后,我们的StringBuilder现在不再接受String作为输入数据类型,而是接受Int作为输入数据类型。幸运的是,我们可以在StringBuilder中实现多个buildExpression(:_)方法,使其同时接受String和Int输入数据类型。继续并添加以下实现,它将使所有错误消失。
- static func buildExpression(_ expression: String) -> String {
- return expression
- }
有了这两种方法,我们现在可以更改greet(name:countdown:)函数的for循环如下所示,所有内容仍将相应地工作。
- @StringBuilder func greet(name: String, countdown: Int) -> String {
- for i in (0...countdown).reversed() {
- // Input an integer instead of a string here.
- i
- }
- // ...
- // ...
- }
- print(greet(name: "Swift Senpai", countdown: 5))
- // Output: 543210HellotoSwift Senpai
启用各种输出数据类型
添加对各种输出数据类型的支持也相当容易。它的工作原理类似于支持各种输入数据类型,但这次我们必须实现buildFinalResult(_:)方法,该方法在最终输出之前添加一个额外的处理层。出于演示目的,让我们的StringBuilder能够输出一个整数,表示最终输出的字符串字符数。
- static func buildFinalResult(_ component: String) -> Int {
- return component.count
- }
同时确保实现以下最终结果方法,这样StringBuilder就不会失去输出字符串的能力。
- static func buildFinalResult(_ component: String) -> String {
- return component
- }
要查看所有操作,我们可以创建Int类型的StringBuilder变量:
- @StringBuilder var greetCharCount: Int {
- "Hello"
- "World"
- }
- print(greetCharCount)
- // Output: 11 (because "HelloWorld" has 11 characters)
6 结果生成器用例
为了演示,我们使用结果生成器创建了一个非常无用的字符串生成器。如果你想看看results builder的一些实际用例,我强烈建议你看看我的另一篇文章:How I Created a DSL for Diffable Section Snapshot using Result Builders[1],以及这篇Antoine van der Lee撰写的:Result builders in Swift explained with code examples[2]。
此外,您还可以查看这个伟大的GitHub repo,它包含大量使用结果构建器构建的项目:awesome-function-builders[3]。
7 总结
我希望这篇文章能让你很好地了解结果生成器是如何工作的。如果您对结果构建器的基本概念仍有疑问,您可以在这里[4]获得完整的示例代码,然后自己进行测试。
参考资料
- [1]How I Created a DSL for Diffable Section Snapshot using Result Builders: https://swiftsenpai.com/swift/result-builders-basics/
- [2]Result builders in Swift explained with code examples: https://www.avanderlee.com/swift/result-builders/
- [3]awesome-function-builders: https://github.com/carson-katri/awesome-function-builders
- [4]这里: https://gist.github.com/LeeKahSeng/ff0bfddc51412b3b288c26c89fcc8489
- [5]Twitter: https://twitter.com/Lee_Kah_Seng
本文转载自微信公众号「 Swift 社区」,可以通过以下二维码关注。转载本文请联系 Swift 社区公众号。