关于swift:Swift-中的-Function-Builder-理解与运用

1次阅读

共计 7073 个字符,预计需要花费 18 分钟才能阅读完成。

Function Builder 是 Swift 5.1 引入的个性,大大加强了 Swift 语言构建内置 DSL 的能力。SwiftUI 申明式 UI 构建形式就是靠的 DSL 实现的。


从 DSL 说起

DSL 是 Domain Specific Language 的缩写,意思就是特定畛域的语言。与之对应的就是咱们相熟的 C, Java, Swift 这些通用的语言,通用语言什么畛域都能够去插一脚,无非就是适不适宜,好不好用罢了。DSL 则是局限在某个特定场景的特地设计过的语言,因为专一,所以业余,它们往往能以十分轻量级的语法和易于了解的形式来解决特定问题。

举几个驰名的 DSL 的例子:

  • 正则表达式

通过一些规定好的符号和组合规定,通过正则表达式引擎来实现字符串的匹配

  • HTML & CSS

尽管写的是相似 XML 或者 .{} 一样的字符规定,然而最终都会被浏览器内核转变成 Dom 树,从而渲染到 Webview 上

  • SQL

诸如 create select insert 这种单词前面跟上参数,这样的语句实现了对数据库的增删改查一系列程序工作

那么这种语言内建的 DSL 有什么益处呢,咱们先来看一个 HTML 的界面搭建:

<div>
  <p>Hello World!</p>
  <p>My name is KY!</p>
</div>

在 UIKit 里要搭建上述界面很显著要麻烦很多:

let container = UIStackView()
container.axis = .vertical
container.distribution = .equalSpacing

let paragraph1 = UILabel()
paragraph1.text = "Hello, World!"

let paragraph2 = UILabel()
paragraph2.text = "My name is KY!"

container.addSubview(paragraph1)
container.addSubview(paragraph2)

这就是申明式 UI 与 命令式 UI 的区别,申明式 UI 用 DSL 来形容“UI 应该是什么样子的”,命令式 UI 则须要先创立一个 View,再指定这个 View 的个性,再指定这个 View 要放到哪里,一步一步来,显得较为轻便。

DSL 能够让 SwiftUI 以相似 HTML 的形式来搭建界面

构建 SwiftUI 的 DSL 语法的除了 Function Builder 还有 Property Wrapper, Opaque Return Type,链式调用 等个性,本文只探讨 Function Builder


Function Builder

Function Builder 实质上是语法糖,没有 Function Builder, Swift 仍然能够构建 DSL,然而会麻烦不少。不必 Function Builder 来构建 DSL 能够参看这篇文章:Building DSLs in Swift

上面,追随 Function Builders in Swift and SwiftUI 这篇文章通过构建一个 AttributedStringBuilder 来学会 FunctionBuilder(翻译)


了解 Function Builder

一个 Function Builder 是一个类型,它实现了一个内置的 DSL,这个 DSL 能够把一个函数内的表达式作为局部后果(partial results)收集起来合并成返回值

最小的 Function Builder 类型实现如下:

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

定义 Function Builder 要用 @_functionBuilder 来润饰,定义好之后就能够作为 Attribute 来用了。

留神下划线,示意这个性能还没有被正式驳回,依然在开发中,当前可能会有变动

这个动态的 buildBlock() 办法是必须的。

一个 function builder attribute 能够被用在两种中央:

  1. func, var 或者 subscript 的申明上,前提是这些申明不是某个协定须要的。
  2. 作为一个函数的闭包参数,能够作为协定的一部分。

其实不管 function builder 用在哪里,它都是将跟在前面的表达式串作为参数传递给它的 buildBlock() 办法,用该办法的返回值就是被标注的理论值

让咱们用下面定义的 @Builder 作为例子来领会下这两种应用场景:

用在申明里代码如下:

@Builder func abc() -> String {
    "Method:"
    "ABC"
}

struct Foo {
    @Builder var abc: String {
        "Getter:"
        "ABC"
    }
    
    @Builder subscript(_ anything: String) -> String {
        "sbscript"
        "ABC"
    }
}

用在闭包参数上代码如下:

func acceptBuilder(@Builder _ builder: () -> String) -> Void {print(builder())
}

运行测试代码:

func testBuilder() -> Void {print(abc())
    print(Foo().abc)
    print(Foo()[""])
    acceptBuilder {
        "Closure Argument:"
        "ABC"
    }
}

打印内容如下:

Method: ABC
Getter: ABC
Subscript: ABC
Closure Argument: ABC

funcion builder 要解决的问题就是结构多层次的异构数据结构。举两个例子来说就是:

  • 生成构造数据,如 XML, JSON 等
  • 生成 GUI 层次结构,如 SwiftUI, HTML 等

这就是 function builder 要做的事,那它是怎么工作的呢?


深刻 Function Builder

如果咱们将办法 abc() 生成的 AST dump 进去能够看到:

(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**)
...

咱们能够发现最初调用的其实是:

Builder.buildBlock("Method:", "ABC")

在语法分析(semantic analysis)阶段,Swift 编译器会将 function builder transforms 这个货色 applies 到 parsed AST 上,就如同咱们曾经写了 Builder.buildBlock(<arguments>) 一样 (1, 2)

另一个例子是 function builder 用作闭包参数的时候。在这种状况下,Swift 编译器会 rewrite the closure to a closure with a single expression body containing the builder invocations.

在某些状况下,一个 function builder 须要提供上面这几个 building 办法来满足不同类型的变形(transformations)须要 (1, 2):

  • buildBlock(_ parts: PartialResult...) -> PartialResult

将局部后果聚合成一个

  • ​buildDo(_ parts: PartialResult...) -> PartialResult

与 ​​buildBlock()​ 一样,只是作用于 ​​do​ 语句

  • ​buildIf(_ part: PartialResult?) -> PartialResult

作用于 ​​if​ 语句,true 时 ​​part 为前面跟的内容转换成的 ​​PartialResult​,false 时 ​​part​ 为 ​​nil

  • ​buildEither(first: PartialResult) -> PartialResult​ 与 ​​buildEither(second: PartialResult) -> PartialResult

作用于 ​​if...else...​ 语句,必须同时实现

  • ​buildExpression(_ expression: Expression) -> PartialResult

把单个的非 ​​PartialResult​ 转换成 ​​PartialResult

  • ​buildOptional(_ part: PartialResult?) -> PartialResult

将一个可空 ​​PartialResult​ 转换成不可空的

  • ​buildFinalResult(_ parts: PartialResult...) -> Result

将多个 ​​PartialResult​ 转换成 ​​Result

所有这些办法都反对基于其参数类型的 overloads

所以呢,Swift 编译器在碰到 function builder 的时候会用上述办法来替换 DSL 语法内容。如果找不到相应的办法,就会报编译谬误


实现定制的 Function Builder

让咱们实现一个 ​​NSAttributedString​ 的 function builder 吧,代码如下:

@_functionBuilder struct AttributedStringBuilder {
    // 根本办法
    static func buildBlock(_ parts: NSAttributedString...) -> NSAttributedString {let result = NSMutableAttributedString(string: "")
        parts.forEach(result.append)
        return result
    }
    
    // String 转成 NSAttributedString
    static func buildExpression(_ text: String) -> NSAttributedString {NSAttributedString(string: text)
    }
    
    // 转 UIImage
    static func buildExpression(_ image: UIImage) -> NSAttributedString {NSAttributedString(attachment: NSTextAttachment(image: image))
    }
    
    // 转本人,不是很分明为什么肯定要这个办法,感觉有下面几个就够了呀,然而实际上没有这个会报错
    static func buildExpression(_ attrString: NSAttributedString) -> NSAttributedString {attrString}
    
    // 反对 if 语句
    static func buildIf(_ attrString: NSAttributedString?) -> NSAttributedString {attrString ?? NSAttributedString()
    }
    
    // 反对 if/else 语句
    static func buildEither(first: NSAttributedString) -> NSAttributedString {first}
    static func buildEither(second: NSAttributedString) -> NSAttributedString {second}
}

为了用起来,还须要一个增加 attributes 的办法和用这个 builder 的便当结构器:

extension NSAttributedString {
    // 帮忙加 Attributes
    func withAttributes(_ attrs: [NSAttributedString.Key : Any]) -> NSAttributedString {let result = NSMutableAttributedString(attributedString: self)
        result.addAttributes(attrs, range: NSRange(location: 0, length: self.length))
        return result
    }
    
    // 以 DSL 形式来初始化
    convenience init(@AttributedStringBuilder builder: () -> NSAttributedString) {self.init(attributedString: builder())
    }
}

接下来咱们要来测试一下这个新的 NSAttributedString,因为 NSAttributedString 是 UIKit 的,所以咱们得把它放在 UILabel 里,再用 ​​UIViewRepresentable​ 包装一下能力用在 SwiftUI 里:

struct AttributedStringRepresentable: UIViewRepresentable {
    
    let attrbutedString: NSAttributedString
    
    func makeUIView(context: Context) -> UILabel {let label = UILabel()
        label.numberOfLines = 0
        label.attributedText = attrbutedString
        return label
    }
    
    func updateUIView(_ uiView: UILabel, context: Context) {}}

SwiftUI 测试代码:

struct AttributedStringView: View {
    let optional = true
    
    var body: some View {
        AttributedStringRepresentable(
            attrbutedString: NSAttributedString {
                NSAttributedString {
                    "Folder"
                    UIImage(systemName: "folder")!
                }
                NSAttributedString { }
                "\n"
                NSAttributedString {
                    "Document"
                    UIImage(systemName: "doc")!
                }
                .withAttributes([.font : UIFont.systemFont(ofSize: 32),
                    .foregroundColor : UIColor.red
                ])
                "\n"
                "Blue One".foregroundColor(.blue)
                    .background(.gray)
                    .underline(.cyan)
                    .font(UIFont.systemFont(ofSize: 20))
                "\n"
                if optional {
                    NSAttributedString {
                        "Hello"
                            .foregroundColor(.red)
                            .font(UIFont.systemFont(ofSize: 10.0))
                          "World"
                            .foregroundColor(.green)
                            .underline(.orange, style: .thick)
                    }
                    UIImage(systemName: "rays")!
                }
                "\n"
                if optional {"It's True".foregroundColor(.magenta)
                        .font(UIFont.systemFont(ofSize: 28))
                } else {"It's False".foregroundColor(.purple)
                }
            }
        )
        .frame(width: 250, height: 250)
    }
}

下面代码里的 .foregroundColor 这些来自于 String 和 NSAttributedString 的 modifiers 扩大

最初展现成果如下:

最初,这里有老外做的一堆 awesome-function-builders,有依赖注入的,有 HTTP Request 的,有用来测试的等等,插个眼,有须要当前能够用

正文完
 0