深入研究SWIFT函数构建器

2020-09-25 09:57:49

可用的基础文章:出于几个不同的原因,SwiftUI Swift的函数构建器特性可以说是该语言最近添加的最有趣的功能之一。函数构建器作为SWIFT 5.1的一部分与SwiftUI一起推出,在启用SwiftUI提供的高度声明性API方面发挥了巨大作用,但仍不是完全发布的语言功能。

本周,让我们更仔细地看看函数构建器,以及它们如何为我们提供一些真正有价值的见解,让我们了解SwiftUI的类似DSL的API是如何在幕后运行的。

这则广告让所有人都可以免费享受Swift by Sundell的所有服务。如果可以,请查看此赞助商,因为这将直接帮助您支持此网站:

:调查、诊断和解决问题的速度最高可提高四倍。无论是崩溃、缓慢的屏幕转换、缓慢的网络调用还是没有响应的UI,Instabug都可以让您利用强大的性能模式来跟踪每个问题的原因。检测特定的应用程序版本、设备或网络连接是否影响用户体验,并发现趋势和峰值。现在就开始,发布您的用户会喜欢的应用程序。

对我来说,要真正理解给定的SWIFT功能是如何工作的,最好的方法之一就是实际使用它来构建一些东西,所以这就是我们要做的。例如,假设我们正在开发一个应用程序,该应用程序包含用于定义各种设置的API-使用如下所示的设置类型:

结构设置{var name:string var value:value}扩展设置{enum value{case bool(Bool)case int(Int)case string(String)case group([Setting])}}。

由于上述类型包括对嵌套设置的支持(通过其组值),因此我们能够使用它来构造层次结构。例如,这里我们为我们的设置创建了一个专用组,这些设置被认为是试验性的:

设设置=[设置(名称:";离线模式";,值:。Bool(False)),设置(名称:";搜索页面大小";,值:。INT(25)),设置(名称:";实验";,值:。组([设置(名称:";默认名称";,值:。字符串(";无标题";)),设置(名称:";流体动画";,值:。Bool(True))]))]

虽然上面的API没有什么问题(事实上,它相当不错!),但让我们看看如果我们对其进行“函数构建器改造”,它最终会是什么样子--这反过来可以让我们将其转换成更像DSL,类似于SwiftUI提供的功能。

顾名思义,SWIFT的函数构建器功能本质上允许我们将函数的内容构建到单个值中。在SwiftUI中,它用于将其众多容器(如HStack或VStack)中的一个容器的内容转换为单个封闭视图,可以通过调用以下容器实例上的type(of:)函数来查看该视图:

导入SwiftUI let栈=VStack{Text(";Hello";)Text(";World&34;)Button(";i';m a Button";){}}//print';VStack<;TupleView<;(Text,Text,Button<;Text>;)>;>;&>;Print(type(of:Stack))。

SwiftUI使用许多不同的函数构建器实现,如ViewBuilder和SceneBuilder,但是由于我们无法查看这些类型的源代码,因此让我们开始为上面介绍的设置API构建我们自己的函数构建器。

就像属性包装器一样,函数构建器被实现为一个普通的SWIFT类型,在本例中使用一个特殊的属性@_functionBuilder进行注释。然后,使用特定的方法名称来实现其各种功能。例如,名为buildBlock且参数为零的方法用于构建空函数或闭包的内容:

然后,上面函数的返回类型(在我们的示例中是设置值的数组)确定我们的构建器可以应用到的函数类型。例如,我们可以选择将顶级设置API实现为全局函数,该函数将新的SettingsBuilder应用于传递给它的任何闭包-如下所示:

完成上述操作后,我们现在可以使用空的尾部闭包调用makeSettings,并且我们将得到一个空数组:

虽然我们的新API还不是很有用,但它已经向我们展示了函数构建器如何工作的几个方面。但现在,让我们真正开始建造东西。

要使SettingsBuilder能够接受输入,我们需要做的就是声明带有与我们希望接收的输入相匹配的参数的buildBlock的额外重载。在我们的示例中,我们只需实现一个接受设置值列表的方法,然后将其作为数组返回-如下所示:

有了新的buildBlock重载后,我们现在可以用设置值填充makeSettings调用,我们的函数构建器(在编译器的一些帮助下)将把所有这些表达式组合到一个数组中,然后返回该数组:

设设置=makeSettings{Setting(Name:";Offline mode";,value:.。Bool(False))设置(名称:";搜索页面大小";,值:。INT(25))设置(名称:";实验";,值:。组([设置(名称:";默认名称";,值:。字符串(";无标题";)),设置(名称:";流体动画";,值:。Bool(True))]))}。

虽然上面可以说已经比我们以前使用的内联数组稍有改进,但让我们继续从SwiftUI中获得灵感,并添加一个由函数构建器支持的API来定义组。为此,我们首先定义一个新的SettingsGroup类型,该类型还使用@SettingsBuilder属性注释闭包,以将其连接到我们的函数构建器:

Struct SettingsGroup{var name:string var设置:[Setting]init(Name:String,@SettingsBuilder builder:()->;[Setting]){sel.。名称=名称自我。设置=builder()}}。

有了上面的内容,我们现在能够以与定义顶级设置完全相同的方式定义组,只需在闭包中表示每个设置-如下所示:

但是,如果我们实际尝试将上述组放入makeSettings闭包中,最终将得到编译器错误-因为我们的函数构建器的buildBlock方法当前需要一个不同的设置值列表,而我们的新SettingsGroup是一个完全不同的类型。

要解决这个问题,让我们引入一个可以在Setting和SettingsGroup之间共享的精简抽象,例如,协议的形式允许我们将这些类型的任何实例转换为设置值的数组:

协议设置可转换{func asSettings()->;[设置]}扩展设置:设置可转换{func asSettings()->;[设置]{[Self]}}扩展设置组:设置可转换{func asSettings()->;[设置]{[设置(名称:名称,值:。集团(设置)]}}。

然后,我们只需修改函数构建器的buildBlock实现,以接受SettingsConverable实例,而不是具体的设置值,并使用flatMap将新参数列表展平:

Extension SettingsBuilder{静态函数buildBlock(_Values:设置可转换...)->;[设置]{值。Flat Map{$0.。AsSettings()}。

有了上面的内容,我们现在可以用一种非常“类似SwiftUI”的方式定义我们的所有设置,方法是构建组,就像我们如何将各种SwiftUI视图组织到堆栈和其他容器中一样:

设设置=makeSettings{Setting(Name:";Offline mode";,value:.。Bool(False))设置(名称:";搜索页面大小";,值:。Int(25))SettingsGroup(名称:";实验";){设置(名称:";默认名称";,值:。字符串(";无标题";))设置(名称:";流体动画";,值:。Bool(True))}}。

真的很好!因此,给定函数构建器包含的buildBlock重载直接决定了我们可以在每个带注释的闭包或函数中放置哪种类型的表达式来使用该构建器。

接下来,让我们看看如何在函数构建器驱动的闭包中添加对条件求值的支持。起初,考虑到SWIFT本身支持各种不同的条件,这看起来似乎应该“就行了”。然而,情况并非如此-因此,对于我们当前的SettingsBuilder实现,如果我们尝试这样做,最终会得到编译器错误:

让我们展示实验:Bool=...。设设置=makeSettings{Setting(Name:";Offline mode";,value:.。Bool(False))设置(名称:";搜索页面大小";,值:。Int(25))//编译器错误:包含控制流语句的闭包//不能与函数生成器';SettingsBuilder';一起使用。如果应该显示实验{SettingsGroup(名称:";实验";){设置(名称:";默认名称";,值:。字符串(";无标题";))设置(名称:";流体动画";,值:。Bool(True))}

上面的代码再次向我们表明,在函数构建器注释的闭包中执行的代码与“普通”SWIFT代码的处理方式不同-因为每个表达式都需要由我们的构建器显式处理,包括if语句等条件语句。

要添加这种处理代码,我们需要实现buildIf方法,编译器会将每个独立IF语句映射到该方法。由于每个这样的语句都可以计算为true或false,因此我们将把它的主体表达式作为可选参数传递-在我们的示例中,它将如下所示:

//这里我们扩展Array使其符合我们的SettingsConverable//协议,以便能够在传递nil值的情况下从//&';buildIf';实现返回空数组:Extension Array:SettingsConverable where element==Setting{func asSettings()->;[Setting]{self}}Extension SettingsBuilder{static func buildIf(_value:SettingsConvertable?)->;SettingsConvertable{value??[]}}。

有了上面的内容,我们之前的if语句现在可以像我们预期的那样工作。但是,我们还要添加对组合If/Else语句的支持,这可以通过实现buildEither方法的两个重载来实现-一个重载首先带有参数标签,另一个重载第二个,每个重载都对应于给定If/Else语句的第一个和第二个分支:

扩展设置Builder{静态函数生成(First:SettingsConverable)->;SettingsConverable{First}静态函数生成(Second:SettingsConverable)->;SettingsConverable{Second}}。

例如,我们现在可以在前面的if语句中添加一个Else子句,以便让用户请求访问我们应用程序的实验设置(如果这些设置尚未显示):

设设置=makeSettings{Setting(Name:";Offline mode";,value:.。Bool(False))设置(名称:";搜索页面大小";,值:。Int(25))如果应该显示实验{SettingsGroup(名称:";实验";){设置(名称:";默认名称";,值:。字符串(";无标题";))设置(名称:";流体动画";,值:。Bool(True))}}Else{设置(名称:";请求实验访问";,值:。Bool(False))}}。

最后,从SWIFT 5.3开始,我们现在刚刚实现的buildOther方法还支持在函数构建器上下文中使用switch语句,而不需要任何额外的构建方法。

例如,为了支持多个访问级别,假设我们希望将上面的shoudShowExamical Boolean重构为枚举。然后,我们只需在makeSettings闭包中打开该枚举,SWIFT编译器就会自动将这些表达式路由到我们的构建中前面的方法之一:

枚举UserAccessLevel{案例限制案例正常案例实验}让AccesssLevel:UserAccessLevel=...。设设置=makeSettings{Setting(Name:";Offline mode";,value:.。Bool(False))设置(名称:";搜索页面大小";,值:。Int(25))交换机访问级别{case.。受限:设置。空()案例。正常:设置(名称:";请求实验访问";,值:。Bool(False))案例。实验:SettingsGroup(名称:";实验";){设置(名称:";默认名称";,值:。字符串(";无标题";))设置(名称:";流体动画";,值:。Bool(True))}。

关于上面的代码,另一件值得注意的事情是,我们在switch语句的.restricted case中使用了一个新的Setting.Empty类型。这是因为我们(目前)还不能在函数构建器switch语句中使用Break关键字,所以我们需要用每个代码分支来表示某种值。因此,就像SwiftUI拥有EmptyView一样,我们的新设置API现在有一个针对这些情况的Setting.Empty类型:

扩展设置{struct Empty:SettingsConvertable{func asSettings()->;[Setting]{[]}。

至此,我们的函数构建器驱动的设置API现在已经完成,虽然在函数构建器功能正式通过SWIFT演进之前,我们可能不应该在生产中使用这样的东西,但使用它构建类似SwiftUI的DSL所需的代码是如此之少,这是相当令人着迷的。

这则广告让所有人都可以免费享受Swift by Sundell的所有服务。如果可以,请查看此赞助商,因为这将直接帮助您支持此网站:

:调查、诊断和解决问题的速度最高可提高四倍。无论是崩溃、缓慢的屏幕转换、缓慢的网络调用还是没有响应的UI,Instabug都可以让您利用强大的性能模式来跟踪每个问题的原因。检测特定的应用程序版本、设备或网络连接是否影响用户体验,并发现趋势和峰值。现在就开始,发布您的用户会喜欢的应用程序。

通过属性包装器和函数构建器等功能,SWIFT正在进入一些非常有趣的新领域,使我们能够将自己的逻辑添加到各种基本语言机制中-例如如何计算表达式,或者如何分配和存储属性。

诚然,这些新功能也确实让SWIFT变得更加复杂,尽管(至少在最好的情况下),它们也可以让库设计师-无论是在苹果还是在更广泛的开发社区-将这种复杂性隐藏在格式良好的API后面。

我个人希望看到函数构建器功能尽快通过SWIFT演进,去掉其关键字前面的下划线,这样我们就可以开始在我们的项目中使用它,而不必担心它们的行为在不同的SWIFT版本之间可能会发生变化。

你认为如何?您期待在代码中使用函数构建器吗?通过阅读本文,您是否对SwiftUI的API如何工作有了更多的了解?如果是这样,请随意分享,如果您有任何问题、评论或反馈,也非常欢迎您与我联系(通过Twitter或电子邮件)。