Swift 和 SwiftUI 中的函数构建器

1,275 阅读3分钟

函数构建器是在 Swift5.1 中新增的语言特性。它使得 Swift 支持 DSL,有了它,我们可以以一种简洁可读的方式构建各种各样的视图层级。

在这篇文章中,我们将会学习以下内容:

  • 什么是函数构建器
  • 函数构建器在 Swift 编译器级别是怎么工作的
  • 怎样实现自定义的 Swift 函数构建器

理解 Swift 函数构建器

函数构建器是一种利用内置的 DSL 定义的类型,它会从函数中的表达式收集结果,然后把它们组合成返回值。

最小的一个函数构建器类型实现如下:

@_functionBuilder struct Builder {
	static func buildBlock(_ partialResults: String...) -> String {
    	partialResults.reduct("", +)
    }
}

函数构建器类型必须用 @functionBuilder 属性声明,这样它才能作为一个自定义属性标签被使用。

buildBlock() 方法是必须实现的,而且必须是 static 的。否则编译器将会报错。

自定义函数构建器标签可以被用在以下地方:

  1. func, var 和 subscript 声明。函数构建器就会转换被标签修饰的方法体。
  2. 作为方法参数的闭包,函数构建器将会转换闭包中的内容,除非闭包中包含 return 语句。

在 func, var 和 subscript 中使用 @Builder

// 应用在函数
@Builder func abc() -> String {
	"Method:"
    "ABC"
}

struct Foo {
	// 应用在属性中
	@Builder var abc: String {
    	"Getter:"
        "ABC"
    }
    
    // 应用在脚本方法中
    subscript(_ anything: String) -> String {
    	@Builder get {
        	"Subscript:"
            "ABC"
        }
        set { /* nothing*/ }
    }
}

结果

// 调用
print(abc())
print(Foo().abc)
print(Foo()[""])

// 打印结果:
Method: ABC
Getter: ABC
Subscript: ABC

在方法闭包参数中使用 @Builder

func acceptBuilder(@Builder build: () -> String) {
	print(build())
}

调用:

acceptBuilder {
    "Closure argument: "
    "ABC"
}

打印结果:

Closure argument: ABC

Swift 函数构建器的目的

Swift 的核心目标是帮助创建优秀的库

函数构建器主要是为了解决:层级数据的构建(译者注:用层级结构的方式描述视图)。例子有:

  • 生成结构化数据:XML,JSON
  • 生成视图层级:SwiftUI, HTML 那么函数构建器是怎么工作的呢?

Swift 函数构建器的结构

我们把 abc() 方法转换成 AST,结果如下:

(func_decl range=[builder.swift:10:10 - line:13:1] "abc()" interface type='() -> String' access=internal
...
  (declref_expr implicit type='(Builder.Type) -> (String...) -> String' location=builder.swift:10:31 range=[builder.swift:10:31 - line:10:31] decl=builder.(file).Builder.buildBlock@builder.swift:5:17 function_ref=single)
  ...
    (string_literal_expr type='String' location=builder.swift:11:5 range=[builder.swift:11:5 - line:11:5] encoding=utf8 value="Method: " builtin_initializer=Swift.(file).String extension.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:) initializer=**NULL**)
    (string_literal_expr type='String' location=builder.swift:12:5 range=[builder.swift:12:5 - line:12:5] encoding=utf8 value="ABC" builtin_initializer=Swift.(file).String extension.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:) initializer=**NULL**)
...

我们发现:编译器把 abc() 转换成了对 Builder.builderBlock("Method:", "ABC") 的调用。

在语义分析阶段,编译器把函数构建器的转换应用到解析过的 AST 上,就好像我们直接写的是Builder.buildBlock(<arguments>)似的。

在另一个应用场景方法闭包参数中,编译器会把闭包重写成仅有一条语句的闭包,这条语句就是调用 Builer 的方法。

函数构建器要提供以下方法中的一部分:

  • buildBlock(_ components: Component...) -> Component 把多种可能的结果组合成一个,大部分情况下都是调用这个函数。
  • buildDo(_ components: Component...) -> Component 与 buildBlock 类似,但是是用在 do 关键字中的,如果没有声明,将会调用 buildBlock()
  • buildIf(_ components: Component...) -> Component 是用在 if 条件语句中的
  • buildEither(first: Component) -> ComponentbuildEither(second: Component) -> Component 配合使用, 对应着 if else 语句。必须两个同时实现。
  • buildOptional(_ component: Component?) -> Component 把可选的输入信息,组合成结果