Swift结果生成器:几个必备的知识点

2021-11-26 14:50:17 浏览数 (1)

1

前言

Preface

结果生成器(以前叫做函数生成器)是swift5.4中引入的一项新feature,它是SwiftUI中支持ViewBuilder的技术。随着Xcode12.5的发布(目前处于beta测试阶段),苹果正式向开发者开放了它,允许我们为各种用例创建自己的自定义结果生成器。

本文讲讲解结果生成器的基本概念工作原理以及如何使用它来创建自己的自定义结果生成器

话不多说,让我们马上开始吧!

2

基本形式

The Basic Form

作为演示,我们创建一个字符串生成器,并使用⭐️ 作为分隔符。例如,给定“Hello”“World”,我们的字符串生成器将返回一个连接的字符串“Hello”⭐️“World”

让我们开始使用结果生成器的最基本形式来构建字符串生成器:

代码语言:javascript复制
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标记函数或变量:

代码语言:javascript复制
// 用 `StringBuilder`标记函数
@StringBuilder func buildStringFunc() -> String {
    
    // components区域
    // ...
}


// 用 `StringBuilder`标记变量
@StringBuilder var buildStringVar: String {
    
    // components区域
    // ...
}

注意上面提到的组件区域,它是向StringBuilder提供所需字符串的地方。components区域中的每一行表示buildBlock(_:)可变参数的一个组件。以下面的StringBuilder为例:

代码语言:javascript复制
@StringBuilder func greet() -> String {
    "Hello"
    "World"
}

print(greet())
// Output: "HelloWorld"

可以翻译为:

代码语言:javascript复制
func greetTranslated() -> String {

    //解析StringBuilder中的所有部分组件`
    let finalOutput = StringBuilder.buildBlock("Hello", "World")

    return finalOutput
}
print(greetTranslated())

小Tip:

您可以在buildBlock(_:)方法中添加print语句,以查看何时触发它以及在任何给定时间提供了哪些组件。

这就是创建结果生成器所需的全部内容。现在您已经看到了一个基本的结果生成器,让我们继续向StringBuilder添加更多的功能。

3

选择语句

The Selection Statements

没有“else”块的“if”语句

假设我们要扩展greet()方法的功能,接受name参数然后根据name来跟用户打招呼。我们可以这样更新greet()方法:

代码语言:javascript复制
@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语句。为了支持没有elseif语句,我们必须将以下结果构建方法添加到StringBuilder中。

代码语言:javascript复制
@resultBuilder
struct StringBuilder {
    
    // ...
    // ...
    
    static func buildOptional(_ component: String?) -> String {
        return component ?? ""
    }
}

它的工作原理是,当满足if语句条件时,把部分结果传递给buildOptional(_:)方法,否则把nil传递给buildOptional(_:)方法。

为了让你更清楚地了解结果生成器是如何解析覆盖下的每个部分组件,上面的greet(name:)函数等效于以下代码段:

代码语言:javascript复制
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:)函数,将得到以下输出:

代码语言:javascript复制
print(greet(name: ""))
// Actual output: HelloWorld
// Expected output: HelloWorld

输出字符串的末尾额外的"⭐️",是由于buildBlock(_:)方法通过buildOptional(_:)方法连接空字符串返回。为了解决这个问题,我们可以简单地更新buildBlock(_:)方法,在连接之前过滤掉组件中的所有空字符串:

代码语言:javascript复制
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:)函数,如下所示:

代码语言:javascript复制
@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块,我们必须实现另外两种结果构建方法:

代码语言:javascript复制
static func buildEither(first component: String) -> String {
    return component
}

static func buildEither(second component: String) -> String {
    return component
}

这两种方法总是结合在一起的。当满足if块条件时,buildery(first:)方法将触发;然而,当满足else块条件时,buildery(second:)方法将触发。下面是一个等价函数,可以帮助您理解场景背后发生的逻辑:

代码语言:javascript复制
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循环

The “for-in” Loops

接下来,让我们更新greet(name:)函数,在问候用户之前倒计时,因为为什么不呢?

0 人点赞