1前言ResultBuilder(原名FunctionBuilder)是swift5.4引入的新特性,是SwiftUI中支持ViewBuilder的一项技术。随着Xcode12.5的发布(目前处于Beta测试阶段),Apple正式向开发者开放,允许我们为各种用例创建自己的自定义结果生成器。本文解释了结果生成器的基本概念、它的工作原理以及如何使用它来创建您自己的自定义结果生成器。事不宜迟,让我们开始吧!2基本形式作为演示,我们创建一个字符串生成器并使用??作为分隔符。例如,给定“Hello”和“World”,我们的字符串构建器将返回一个连接的字符串“Hello”??“World”。让我们开始使用结果构建器的最基本形式构建字符串构建器:resultBuilderstructStringBuilder{staticfuncbuildBlock(_components:String...)->String{returncomponents.joined(separator:"")}}您可以使用@resultBuilder来完成此操作该属性标记自定义结构并强制执行buildBlock(_:)静态方法来定义生成的构建器。buildBlock(_:)方法类似于StringBuilder的入口点,它接受组件的可变参数,这意味着它可以是1个或多个字符串。在buildBlock(_:)方法中,我们可以对给定的组件做任何我们想做的事情。在这个例子中,我们将使用“??”作为分隔符。在实现buildBlock(_:)方法时,需要遵循一个规则:返回的数据类型必须与组件数据类型相匹配。以StringBuilder为例,buildBlock(_:)方法组件是String类型,那么它的返回类型也必须是String。要创建StringBuilder实例,您可以使用@StringBuilder标记一个函数或变量://使用`StringBuilder`标记一个函数@StringBuilderfuncbuildStringFunc()->String{//componentsarea//...}//使用markavariable`StringBuilder`@StringBuildervarbuildStringVar:String{//componentsarea//...}请注意上面提到的组件区域,它是将所需字符串提供给StringBuilder的地方。组件区域中的每一行代表buildBlock(_:)可变参数的一个组件。以下面的StringBuilder为例:@StringBuilderfuncgreet()->String{"Hello""World"}print(greet())//输出:"HelloWorld"可以翻译为:funcgreetTranslated()->String{//StringBuilder分析`letfinalOutput=StringBuilder.buildBlock("Hello","World")returnfinalOutput}print(greetTranslated())中的所有部分组件小提示:可以在buildBlock(_:)方法中添加打印语句,看看什么时候它被触发它以及哪些组件在任何给定时间可用。这就是创建结果生成器所需的全部。现在您已经看到了一个基本的结果生成器,让我们继续向StringBuilder添加更多功能。3Select语句没有“else”块的“if”语句假设我们想扩展greet()方法的功能以接受名称参数,然后按名称问候用户。我们可以像这样更新greet()方法:@StringBuilderfuncgreet(name:String)->String{"Hello""World"if!name.isEmpty{"to"name}}print(greet(name:"SwiftSenpai"))/像这样修改/Expectedoutput:"HelloWorldtoSwiftSenpai"之后,你应该会看到编译器开始抱怨:包含控制流语句的闭包不能与结果生成器'StringBuilder'一起使用这是因为我们的StringBuilder目前不理解什么是if语句是。为了支持没有else的if语句,我们必须在StringBuilder中添加如下结果构建方法。@resultBuilderstructStringBuilder{//...//...staticfuncbuildOptional(_component:String?)->String{returncomponent??""}}它的工作原理是在if语句的条件时将部分结果传递给buildOptional满足(_:)方法,否则将nil传递给buildOptional(_:)方法。为了让您更清楚地了解结果生成器如何解析覆盖下的每个部分组件,上面的greet(name:)函数等效于以下代码片段:funcgreetTranslated(name:String)->String{//Resolveallpartialcomponentswithinthe`if`blockvarpartialComponent1:String?if!name.isEmpty{partialComponent1=StringBuilder.buildBlock("to",name)}//Resolvetheentire`if`blockletpartialComponent2=StringBuilder.buildOptional(partialComponent1)//Resolveallpartialcomponentsin`StringBuilder`letfinalOutput=StringBuilder.buildBlock("Hello","World",partialComponent2)returnfinalOutput}print(greetTranslated(name:"SwiftSenpai"))//Output:"HelloWorldtoSwiftSenpai"请注意结果生成器如何首先解析if块中的内容,然后递归传递它并解析部分组件,直到获得最终输出。此行为很重要,因为它实质上演示了结果生成器如何解析组件区域中的所有组件。小提示:添加buildOptional(_:)方法不仅支持不带else块的if语句,还支持可选绑定。此时,如果你尝试用空名称调用greet(name:)函数,你将得到如下输出:print(greet(name:""))//Actualoutput:HelloWorld//Expectedoutput:HelloWorldTheextra"在输出字符串的末尾??”,因为buildBlock(_:)方法通过buildOptional(_:)方法返回一个空字符串。为了解决这个问题,我们可以简单地更新buildBlock(_:)方法以在连接它们之前过滤掉组件中的任何空字符串:staticfuncbuildBlock(_components:String...)->String{letfiltered=components.filter{$0!=""}returnfiltered.joined(separator:"")}"if"statementwith"else"block我们的StringBuilder现在比以前更聪明了,但是说"Hello??World??to??"SwiftSenpai"Listen听起来很奇怪。让我们让它更聪明,如果name不为空,它会打印“Hello??to??[name]”,否则打印“Hello??World”。继续更新greet(name:)函数,如下所示:@StringBuilderfuncgreet(name:String)->String{"Hello"if!name.isEmpty{"to"name}else{"World"}}print(greet(name:"SwiftSenpai"))//Expectedoutput:"HellotoSwiftSenpai"你会再次看到编译错误:Closurecontainingcontrolflowstatementcannotbeusedwithresultbuilder'StringBuilder'这一次,我们必须实现另外两个结果构建方法,因为额外的else块:staticfuncbuildEither(firstcomponent:String)->String{returncomponent}staticfuncbuildEither(secondcomponent:String)->String{returncomponent}这两个方法总和。buildery(first:)方法将在满足if块的条件时触发;但是,buildery(second:)方法将在满足else块的条件时触发。下面是一个等效函数,可帮助您理解场景背后的逻辑:funcgreetTranslated(name:String)->String{varpartialComponent2:String!if!name.isEmpty{//Resolveallpartialcomponentswithinthe`if`blockletpartialComponent1=StringBuilder.buildBlock("to",name)//Resolvetheentire`if-else`blockpartialComponent2=StringBuilder.buildEither(first:partialComponent1)}else{//Resolveallpartialcomponentswithinthe`else`blockletpartialComponent1=StringBuilder.buildBlock("World")//Resolvetheentire`if-else`blockpartialComponent2=StringBuilder.buildEither(second:partialComponent1)}//Resolveallpartialcomponentsin`StringBuilder`letfinalOutput=StringBuilder.buildBlock("Hello",partialComponent2)returnfinalOutput}print(greetTranslated(name:"SwiftSenpai"))//Output:"Helloto"4SwiftSenpaifor-在循环中接下来,让我们更新greet(name:)函数以在问候用户之前倒计时,因为为什么不呢?🤷🏻?♂?继续更新greet(name:)函数,如下所示:@StringBuilderfuncgreet(name:String,countdown:Int)->String{foriin(0...countdown).reversed(){"\(i)"}"Hello"if!name.isEmpty{"to"name}else{"World"}}print(greet(name:"SwiftSenpai",countdown:5))//Expectedoutput:543210HellotoSwiftSenpai注意我在函数的开头添加了一个倒计时参数和for循环for循环将执行从给定值到0的倒计时。下一步也是最后一步是用以下内容更新StringBuilder结果构建方法:staticfuncbuildArray(_components:[String])->String{returncomponents.joined(separator:"")}注意buildArray(_:)方法与结果构建方法的相同,其余略有不同,它需要一个数组作为输入。在幕后发生的是,在每次迭代结束时,for循环将生成一个字符串(部分组件)。在所有迭代完成后,每次迭代的结果将被分组到一个数组中并传递给buildArray(_:)方法。为了更好地说明流程,这里是等效函数:funcgreetTranslated(name:String,countdown:Int)->String{//ResolvepartialcomponentsineachiterationvarpartialComponents=[String]()foriin(0...countdown).reversed(){letcomponent=StringBuilder.buildBlock("\(i)")partialComponents.append(component)}//Resolvetheentire`for-in`loopletloopComponent=StringBuilder.buildArray(partialComponents)//`if-else`blockprocessinghere//...//...//...//Resolveallpartialcomponentsin`StringBuilder`letfinalOutput=StringBuilder.buildBlock(loopComponent,"Hello",partialComponent2)returnfinalOutput}print(greetTranslated(name:"SwiftSenpai",countdown:5))//Output:543210HellotoSwiftSenpaiWith它,我们的StringBuilder可以处理for-in循环。现在尝试运行代码,您将在Xcode控制台中看到“543210??Hello??to??SwiftSenpai”。注意:添加buildArray(_:)方法将不支持while循环。事实上,for-in循环是结果生成器唯一支持的循环方法。5支持不同的数据类型在这个阶段,我们已经使StringBuilder变得非常灵活,它现在可以接受select语句、for循环和可选绑定作为输入。但是,有一个很大的限制:它只能支持字符串作为输入和输出数据类型。幸运的是,支持各种输入和输出数据类型非常简单。我会告诉你如何去做。启用各种输入数据类型假设我们希望StringBuilder支持Int作为输入类型,我们可以在StringBuilder中添加以下结果构建方法:staticfuncbuildExpression(_expression:Int)->String{return"\(expression)"}ThisbuildExpression(_:)方法是可选的,它接受一个整数作为输入并返回一个字符串。一旦实现,它就成为结果生成器的入口点并充当适配器,将其输入数据类型转换为buildBlock(:_)方法接受的类型。这就是为什么您会看到多个“无法将‘String’类型的值转换为预期的参数类型‘Int’”错误添加buildExpression(:_)方法后,我们的StringBuilder现在不再接受String作为输入数据类型,而是接受Int作为输入数据类型。幸运的是,我们可以在StringBuilder中实现多个buildExpression(:_)方法来接受String和Int输入数据类型。继续并添加以下实现,它将使所有错误消失。staticfuncbuildExpression(_expression:String)->String{returnexpression}有了这两个方法,我们现在可以将greet(name:countdown:)函数的for循环更改为如下所示,一切仍将相应地工作。@StringBuilderfuncgreet(name:String,countdown:Int)->String{foriin(0...countdown).reversed(){//Inputanintegerinsteadofstringhere.i}//...//...}print(greet(name:"SwiftSenpai",countdown:5))//Output:543210HellotoSwiftSenpai启用各种输出数据类型添加对各种输出数据类型的支持也相当容易。它的工作方式类似于支持各种输入数据类型,但是这次我们必须实现buildFinalResult(_:)方法,它在最终输出之前添加了一个额外的处理层。出于演示目的,让我们的StringBuilder输出一个整数,表示最终输出字符串中的字符数。staticfuncbuildFinalResult(_component:String)->Int{returncomponent.count}还要确保实现以下最终结果方法,这样StringBuilder就不会失去输出字符串的能力。staticfuncbuildFinalResult(_component:String)->String{returncomponent}要查看所有操作,我们可以创建一个Int类型的StringBuilder变量:@StringBuildervargreetCharCount:Int{"Hello""World"}print(greetCharCount)//Output:11(因为"HelloWorld"has11characters)6ResultBuilder用例为了演示,我们使用resultbuilder创建一个非常无用的字符串生成器。如果您想查看结果生成器的一些实际用例,我强烈建议您看一下我的另一篇文章:我如何使用结果生成器[1]为可区分部分快照创建DSL,以及Antoinevander的这篇文章Lee:Swift中的结果构建器通过代码示例进行了解释[2]。此外,您还可以查看这个很棒的GitHub存储库,其中包含大量使用结果构建器构建的项目:awesome-function-builders[3]。7总结我希望这篇文章能让你很好地理解结果生成器的工作原理。如果您对结果生成器的基本概念仍有疑问,您可以在此处[4]获取完整的示例代码并自行测试。参考[1]HowICreatedaDSLforDiffableSectionSnapshotusingResultBuilders:https://swiftsenpai.com/swift/result-builders-basics/[2]ResultbuildersinSwift解释代码示例: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]推特:https://twitter.com/Lee_Kah_Seng本文转载自微信公众号「Swift社区」,可通过以下二维码关注。转载本文请联系Swift社区公众号。
