Swift Result Builders:你需要知道的基础知识!

Swift Result Builders:你需要知道的基础知识!

hudson 译 原文

结果构建器(以前称为函数构建器)是Swift 5.4中引入的一项新功能,它是在SwiftUI中增强ViewBuilder的技术。随着Xcode 12.5(目前处于beta测试阶段)的发布,苹果已将其正式提供给开发人员,允许我们为各种用例创建自己的自定义结果构建器。

在本文中,我想与您分享结果构建器的基本概念,它的工作原理,以及如何使用它来创建自己的自定义结果构建器。

不用多说,让我们直接进入它吧!

基本形式

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

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

swift 复制代码
@resultBuilder
struct StringBuilder {
    
    static func buildBlock(_ components: String...) -> String {
        return components.joined(separator: "⭐️")
    }
}

如您所见,您可以通过使用@resultBuilder属性标记自定义结构,并实现必须的buildBlock(_:)静态方法来定义结果生成器。

buildBlock(_:)方法就像我们StringBuilder的入口点,它接受一组可变长度的变量参数,这意味着它可以是1个或多个字符串。在buildBlock(_:)方法中,我们可以对给定的组件进行任何我们想要的处理,就我们情形而言,我们将使用"⭐️"作为分隔符连接给定的字符串。

在实现buildBlock(_:)方法时,有一个规则需要遵循:返回的数据类型必须与组件数据类型匹配 。以StringBuilder为例,buildBlock(_:)方法的组件是String类型,因此,其返回类型也必须是String

要创建StringBuilder的实例,我们可以使用@StringBuilder标记函数或变量:

swift 复制代码
// Mark a function as `StringBuilder`
@StringBuilder func buildStringFunc() -> String {
    
    // Here's the components area
    // ...
}
swift 复制代码
// Mark a variable as `StringBuilder`
@StringBuilder var buildStringVar: String {
    
    // Here's the components area
    // ...
}

请注意上述组件区域 ,这是您为StringBuilder提供所需字符串的地方。组件区域中的每行将代表buildBlock(_:) 变参的1个组件。

让我们以以下StringBuilder为例:

swift 复制代码
@StringBuilder func greet() -> String {
    "Hello"
    "World"
}
print(greet())
// Output: "Hello⭐️World"

它可以转换为:

swift 复制代码
func greetTranslated() -> String {

    // Resolve all partial components in `StringBuilder`
    let finalOutput = StringBuilder.buildBlock("Hello", "World")

    return finalOutput
}
print(greetTranslated())

专业提示:

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

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

选择语句

没有"else"语句块的"if"语句

假设我们想扩展greet()方法的功能,以接受名称参数并按名称问候用户。我们可以按以下方式更新greet()方法:

swift 复制代码
@StringBuilder func greet(name: String) -> String {
    "Hello"
    "World"

    if !name.isEmpty {
        "to"
        name
    }
}
print(greet(name: "Swift Senpai")) 
// Expected output: "Hello⭐️World⭐️to⭐️Swift Senpai"

通过上述更改,您应该看到编译器开始抱怨:

bash 复制代码
Closure containing control flow statement cannot be used with result builder 'StringBuilder'
(包含控制流语句的闭包不能与结果构建器"StringBuilder"一起使用)

这是因为我们的StringBuilder目前不理解什么是if语句。为了支持没有else语句的if语句,我们必须将以下方法添加到我们的StringBuilder中。

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

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

为了让您更清楚地了解结果生成器在幕后是如何解析每个部分组件的,我们将上面的 greet(name:)函数转换为以下代码片段:

swift 复制代码
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: "Hello⭐️World⭐️to⭐️Swift Senpai"

注意结果生成器如何首先解析if块中的任何内容,然后递归传播和解析部分组件,直到它获得最终输出。这种行为非常重要,因为它从根本上展示了结果生成器如何解析组件区域内的所有组件。

专业提示:

添加buildOptional(_:)方法不仅可以支持没有else语句块的if语句,还可以支持可选绑定

此时,如果您尝试用空名称调用greet(name:)函数,您将获得以下输出:

swift 复制代码
print(greet(name: ""))
// Actual output: Hello⭐️World⭐️
// Expected output: Hello⭐️World

输出字符串末尾的额外"⭐️"是由于buildBlock(_:)方法连接了buildOptional(_:)方法返回的空字符串。

要解决这个问题,我们可以简单地更新buildBlock(_:)方法,在连接之前从组件中过滤掉所有空字符串:

swift 复制代码
static func buildBlock(_ components: String...) -> String {
    let filtered = components.filter { $0 != "" }
    return filtered.joined(separator: "⭐️")
}

带有"else"语句块的"if"语句

我们的StringBuilder现在比以前更聪明了,但说"Hello⭐️World⭐️to⭐️Swift Senpai"听起来很奇怪。

让我们让它更聪明,这样当名称不是空时,它将输出"Hello⭐️to⭐️[name]",否则它将输出"Hello⭐️World"

继续更新greet(name:)功能,如下所示:

swift 复制代码
@StringBuilder func greet(name: String) -> String {
    "Hello"

    if !name.isEmpty {
        "to"
        name
    } else {
        "World"
    }
}
print(greet(name: "Swift Senpai"))
// Expected output: "Hello⭐️to⭐️Swift Senpai"

您将再次看到编译错误:

swift 复制代码
Closure containing control flow statement cannot be used with result builder 'StringBuilder'
(包含控制流语句的闭包不能与结果构建器"StringBuilder"一起使用)

这一次,由于额外的else语句块,我们将不得不实现另外2种结果构建方法:

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

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

这两种方法总是同时出现。 buildEither(first:)方法将在满足if语句块条件时触发;而buildEither(second:) 方法将在满足else语句块条件时触发。

这里有一个等效的函数,可以帮助您理解幕后发生的逻辑:

swift 复制代码
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: "Hello⭐️to⭐️Swift Senpai"

支持"for-in" 循环

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

继续更新greet(name:)函数,如下所示:

swift 复制代码
@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: 543210⭐️Hello⭐️to⭐️Swift Senpai

请注意,我已向函数添加了countdown倒计时参数,并在函数开始时添加了for循环。For循环将从给定的countdown倒计时值执行倒计时到0。

接下来也是最后一件事是使用以下结果构建方法更新我们的StringBuilder

swift 复制代码
static func buildArray(_ components: [String]) -> String {
    return components.joined(separator: "")
}

请注意,buildArray(_:)方法与其他结果构建方法略有不同,它采用数组作为输入。

幕后发生的事情是,在每次迭代结束时,for循环将产生一个字符串(部分组件)。完成所有迭代后,每次迭代的结果将被分组为一个数组,并将其传递给buildArray(_:)方法。

为了更好地说明流程,以下是等效函数:

swift 复制代码
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: 543210⭐️Hello⭐️to⭐️Swift Senpai

有了这一点,我们的StringBuilder现在能够处理for-in循环。现在尝试运行代码,您应该看到Xcode控制台上打印的是 "543210⭐️Hello⭐️to⭐️Swift Senpai"

注意:

添加buildArray(_:)方法不会支持while循环。事实上,for-in循环是结果构建器支持的唯一循环方法。

支持不同的数据类型

在这个阶段,我们已经使我们的StringBuilder非常灵活,它现在可以接受选择语句、循环和可选绑定作为输入。然而,有一个很大的限制------它只能支持字符串作为输入和输出数据类型。

幸运的是,支持各种输入和输出数据类型非常简单。让我告诉你怎么做。

支持不同的输入数据类型

假设我们想让我们的StringBuilder支持Int作为输入类型,我们可以将以下结果构建方法添加到我们的StringBuilder中:

swift 复制代码
static func buildExpression(_ expression: Int) -> String {
    return "\(expression)"
}

buildExpression(_:)方法是可选的,它接受一个整数作为输入并返回一个字符串。一旦实现,它将成为结果构建器的入口点,并充当适配器,将其输入数据类型转换为buildBlock(_:)方法接受的数据类型。

这就是为什么在我们添加buildExpression(_:)方法后,您将看到多个 " Cannot convert value of type 'String' to expected argument type 'Int'"错误,我们的StringBuilder现在不再接受String作为输入数据类型,而是接受Int作为输入数据类型。

幸运的是,我们可以在StringBuilder中实现多个buildExpression(_:)方法,使其同时接受StringInt输入数据类型。继续添加以下实现,它应该会使所有错误消失。

swift 复制代码
static func buildExpression(_ expression: String) -> String {
    return expression
}

在这两种方法到位的情况下,我们现在可以更改greet(name:countdown:)函数的循环,如下所示,一切都将相应地工作。

swift 复制代码
@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: 543210⭐️Hello⭐️to⭐️Swift Senpai

支持不同的输出数据类型

添加对各种输出数据类型的支持也非常容易。它的工作原理类似于支持各种输入数据类型,但这次我们必须实现buildFinalResult(_:)方法,该方法在最终输出之前添加额外的处理层。

为了演示,让我们的StringBuilder能够输出一个表示最终输出字符串字符数的整数。

swift 复制代码
static func buildFinalResult(_ component: String) -> Int {
    return component.count
}

确保也实现以下最终结果方法,以便我们的StringBuilder不会失去输出字符串的能力。

swift 复制代码
static func buildFinalResult(_ component: String) -> String {
    return component
}

要查看所有操作情况,我们可以创建一个Int类型的StringBuilder变量:

swift 复制代码
@StringBuilder var greetCharCount: Int {
    "Hello"
    "World"
}

print(greetCharCount)
// Output: 11 (because "Hello⭐️World" has 11 characters)

结果构建器用例

为了演示,我们使用结果生成器创建了一个相当无用的字符串生成器。如果您想查看结果生成器的一些实际用例,我强烈建议您查看我的另一篇文章------我如何使用结果生成器为可差异节快照创建DSL ,以及Antoine van der Lee的这篇文章------Swift中的结果生成器代码示例

此外,您还可以查看这个很棒的GitHub库,其中包含有使用结果构建器构建的大量项目: awesome-fun to-builders

小结

我希望这篇文章能让你很好地了解结果生成器幕后是如何工作的。如果您仍然对结果构建器的基本概念有疑问,您可以在这里获得完整的示例代码并自己进行测试。

感谢您的阅读。👨🏻‍💻

相关推荐
Swift社区17 小时前
Excel 列名称转换问题 Swift 解答
开发语言·excel·swift
东坡肘子2 天前
肘子的 Swift 周报 #063|异种肾脏移植取得突破
swiftui·swift·apple
威化饼的一隅2 天前
【多模态】swift-3框架使用
人工智能·深度学习·大模型·swift·多模态
opentogether4 天前
Swift 的动态性
开发语言·ssh·swift
苍墨穹天5 天前
SWIFT基本使用
linux·swift
SchneeDuan5 天前
从源码分析swift GCD_DispatchGroup
ios·swift·源码分析·gcd
请叫我飞哥@7 天前
iOS在项目中设置 Dev、Staging 和 Prod 三个不同的环境
ios·xcode·swift
Cedric_Anik9 天前
iOS渲染概述
ui·ios·swift
hxx2219 天前
iOS swift开发系列--如何给swiftui内容视图添加背景图片显示
ios·swiftui·swift
胖虎110 天前
SwiftUI - (十九)组合视图
ios·swiftui·swift·组合视图