Swift結(jié)果生成器:幾個必備的知識點
1 前言
結(jié)果生成器(以前叫做函數(shù)生成器)是swift5.4中引入的一項新feature,它是SwiftUI中支持ViewBuilder的技術(shù)。隨著Xcode12.5的發(fā)布(目前處于beta測試階段),蘋果正式向開發(fā)者開放了它,允許我們?yōu)楦鞣N用例創(chuàng)建自己的自定義結(jié)果生成器。
本文講講解結(jié)果生成器的基本概念、工作原理以及如何使用它來創(chuàng)建自己的自定義結(jié)果生成器。
話不多說,讓我們馬上開始吧!
2 基本形式
作為演示,我們創(chuàng)建一個字符串生成器,并使用⭐️ 作為分隔符。例如,給定“Hello”和“World”,我們的字符串生成器將返回一個連接的字符串“Hello”⭐️“World”。
讓我們開始使用結(jié)果生成器的最基本形式來構(gòu)建字符串生成器:
- resultBuilder
- struct StringBuilder {
- static func buildBlock(_ components: String...) -> String {
- return components.joined(separator: "")
- }
- }
你可以通過使用@resultBuilder屬性標(biāo)記自定義結(jié)構(gòu)體,并強制實現(xiàn)buildBlock(_:)靜態(tài)方法來定義結(jié)果生成器。
buildBlock(_:)方法類似于StringBuilder的入口點,它接受組件的可變參數(shù),這意味著它可以是1個或多個字符串。在buildBlock(_:)方法中,我們可以對給定的組件進(jìn)行任何處理。在這個例子中,我們將使用 "⭐️"作為分隔符。
在實現(xiàn)buildBlock(_:)方法時,需要遵循一條規(guī)則:返回的數(shù)據(jù)類型必須與components數(shù)據(jù)類型匹配。以StringBuilder為例,buildBlock(_:)方法組件是String類型的,因此其返回類型也必須是String。
要創(chuàng)建StringBuilder實例,可以使用@StringBuilder標(biāo)記函數(shù)或變量:
- // 用 `StringBuilder`標(biāo)記函數(shù)
- @StringBuilder func buildStringFunc() -> String {
- // components區(qū)域
- // ...
- }
- // 用 `StringBuilder`標(biāo)記變量
- @StringBuilder var buildStringVar: String {
- // components區(qū)域
- // ...
- }
注意上面提到的組件區(qū)域,它是向StringBuilder提供所需字符串的地方。components區(qū)域中的每一行表示buildBlock(_:)可變參數(shù)的一個組件。以下面的StringBuilder為例:
- @StringBuilder func greet() -> String {
- "Hello"
- "World"
- }
- print(greet())
- // Output: "HelloWorld"
可以翻譯為:
- func greetTranslated() -> String {
- //解析StringBuilder中的所有部分組件`
- let finalOutput = StringBuilder.buildBlock("Hello", "World")
- return finalOutput
- }
- print(greetTranslated())
小Tip:您可以在buildBlock(_:)方法中添加print語句,以查看何時觸發(fā)它以及在任何給定時間提供了哪些組件。
這就是創(chuàng)建結(jié)果生成器所需的全部內(nèi)容。現(xiàn)在您已經(jīng)看到了一個基本的結(jié)果生成器,讓我們繼續(xù)向StringBuilder添加更多的功能。
3 選擇語句
沒有“else”塊的“if”語句
假設(shè)我們要擴展greet()方法的功能,接受name參數(shù)然后根據(jù)name來跟用戶打招呼。我們可以這樣更新greet()方法:
- @StringBuilder func greet(name: String) -> String {
- "Hello"
- "World"
- if !name.isEmpty {
- "to"
- name
- }
- }
- print(greet(name: "Swift Senpai"))
- // Expected output: "HelloWorldtoSwift Senpai"
這樣修改以后,你應(yīng)該會看到編譯器開始抱怨:
- Closure containing control flow statement cannot be used with result builder 'StringBuilder'
- 包含控制流語句的閉包不能與結(jié)果生成器“StringBuilder”一起使用
這是因為我們的StringBuilder目前不理解什么是if語句。為了支持沒有else的if語句,我們必須將以下結(jié)果構(gòu)建方法添加到StringBuilder中。
- @resultBuilder
- struct StringBuilder {
- // ...
- // ...
- static func buildOptional(_ component: String?) -> String {
- return component ?? ""
- }
- }
它的工作原理是,當(dāng)滿足if語句條件時,把部分結(jié)果傳遞給buildOptional(_:)方法,否則把nil傳遞給buildOptional(_:)方法。
為了讓你更清楚地了解結(jié)果生成器是如何解析覆蓋下的每個部分組件,上面的greet(name:)函數(shù)等效于以下代碼段:
- 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"
注意結(jié)果生成器是如何首先解析if塊中的任何內(nèi)容,然后遞歸地傳遞和解析部分組件,直到它獲得最終輸出的。此行為非常重要,因為它從根本上演示了結(jié)果生成器如何解析components區(qū)域中的所有組件。
小Tip:添加buildOptional(_:)方法不僅支持沒有else塊的if語句,還支持可選綁定。
此時,如果嘗試使用空的name調(diào)用greet(name:)函數(shù),將得到以下輸出:
- print(greet(name: ""))
- // Actual output: HelloWorld
- // Expected output: HelloWorld
輸出字符串的末尾額外的"⭐️",是由于buildBlock(_:)方法通過buildOptional(_:)方法連接空字符串返回。為了解決這個問題,我們可以簡單地更新buildBlock(_:)方法,在連接之前過濾掉組件中的所有空字符串:
- static func buildBlock(_ components: String...) -> String {
- let filtered = components.filter { $0 != "" }
- return filtered.joined(separator: "")
- }
帶有“else”塊的“if”語句
我們的StringBuilder現(xiàn)在比以前更聰明了,但是說“Hello⭐️World⭐️to⭐️“Swift Senpai”聽起來怪怪的。
讓我們把它變得更聰明,當(dāng)name不為空時它就可以輸出"Hello⭐️to⭐️[name]",否則輸出 "Hello⭐️World"。繼續(xù)更新greet(name:)函數(shù),如下所示:
- @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'
- 包含控制流語句的閉包不能與結(jié)果生成器“StringBuilder”一起使用
這一次,由于額外的else塊,我們必須實現(xiàn)另外兩種結(jié)果構(gòu)建方法:
- static func buildEither(first component: String) -> String {
- return component
- }
- static func buildEither(second component: String) -> String {
- return component
- }
這兩種方法總是結(jié)合在一起的。當(dāng)滿足if塊條件時,buildery(first:)方法將觸發(fā);然而,當(dāng)滿足else塊條件時,buildery(second:)方法將觸發(fā)。下面是一個等價函數(shù),可以幫助您理解場景背后發(fā)生的邏輯:
- 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循環(huán)
接下來,讓我們更新greet(name:)函數(shù),在問候用戶之前倒計時,因為為什么不呢?🤷🏻♂️
繼續(xù)更新greet(name:)函數(shù),如下所示:
- @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: 543210HellotoSwift Senpai
注意,我在函數(shù)的開頭添加了一個倒計時參數(shù)和for循環(huán)。for循環(huán)將執(zhí)行從給定值到0的倒計時。
下一步也是最后一步是使用以下結(jié)果生成方法更新StringBuilder:
- static func buildArray(_ components: [String]) -> String {
- return components.joined(separator: "")
- }
請注意,buildArray(_:)方法與結(jié)果生成方法的其余部分稍有不同,它將數(shù)組作為輸入。在場景后面發(fā)生的是,在每次迭代結(jié)束時,for循環(huán)將生成一個字符串(部分組件)。在經(jīng)歷了所有迭代之后,每個迭代的結(jié)果將被分組為一個數(shù)組,并將其傳遞給buildArray(_:)方法。為了更好地說明流程,下面是等效函數(shù):
- 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: 543210HellotoSwift Senpai
有了它,我們的StringBuilder就能夠處理for-in循環(huán)?,F(xiàn)在試著運行代碼,你會看到在Xcode控制臺打印"543210⭐️Hello⭐️to⭐️Swift Senpai"。
注:
添加buildArray(_:)方法將不支持while 循環(huán)。實際上,for-in 循環(huán)是結(jié)果生成器支持的唯一循環(huán)方法。
5 支持不同的數(shù)據(jù)類型
在這個階段,我們已經(jīng)使StringBuilder非常靈活,它現(xiàn)在可以接受選擇語句、for循環(huán)和可選綁定作為輸入。但是,有一個很大的限制:它只能支持字符串作為輸入和輸出數(shù)據(jù)類型。
幸運的是,支持各種輸入和輸出數(shù)據(jù)類型非常簡單。我來教你怎么做。
啟用各種輸入數(shù)據(jù)類型
假設(shè)我們想讓StringBuilder支持Int作為輸入類型,我們可以將以下結(jié)果構(gòu)建方法添加到StringBuilder中:
- static func buildExpression(_ expression: Int) -> String {
- return "\(expression)"
- }
此buildExpression(_:)方法是可選的,它接受整型作為輸入并返回字符串。一旦實現(xiàn),它將成為結(jié)果生成器的入口點,并充當(dāng)適配器,將其輸入數(shù)據(jù)類型轉(zhuǎn)換為buildBlock(:_)方法接受的數(shù)據(jù)類型。
這就是為什么您會看到多個“Cannot convert value of type'String'to expected argument type'Int'”錯誤出現(xiàn)在我們添加了buildExpression(:_)方法之后,我們的StringBuilder現(xiàn)在不再接受String作為輸入數(shù)據(jù)類型,而是接受Int作為輸入數(shù)據(jù)類型。幸運的是,我們可以在StringBuilder中實現(xiàn)多個buildExpression(:_)方法,使其同時接受String和Int輸入數(shù)據(jù)類型。繼續(xù)并添加以下實現(xiàn),它將使所有錯誤消失。
- static func buildExpression(_ expression: String) -> String {
- return expression
- }
有了這兩種方法,我們現(xiàn)在可以更改greet(name:countdown:)函數(shù)的for循環(huán)如下所示,所有內(nèi)容仍將相應(yīng)地工作。
- @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: 543210HellotoSwift Senpai
啟用各種輸出數(shù)據(jù)類型
添加對各種輸出數(shù)據(jù)類型的支持也相當(dāng)容易。它的工作原理類似于支持各種輸入數(shù)據(jù)類型,但這次我們必須實現(xiàn)buildFinalResult(_:)方法,該方法在最終輸出之前添加一個額外的處理層。出于演示目的,讓我們的StringBuilder能夠輸出一個整數(shù),表示最終輸出的字符串字符數(shù)。
- static func buildFinalResult(_ component: String) -> Int {
- return component.count
- }
同時確保實現(xiàn)以下最終結(jié)果方法,這樣StringBuilder就不會失去輸出字符串的能力。
- static func buildFinalResult(_ component: String) -> String {
- return component
- }
要查看所有操作,我們可以創(chuàng)建Int類型的StringBuilder變量:
- @StringBuilder var greetCharCount: Int {
- "Hello"
- "World"
- }
- print(greetCharCount)
- // Output: 11 (because "HelloWorld" has 11 characters)
6 結(jié)果生成器用例
為了演示,我們使用結(jié)果生成器創(chuàng)建了一個非常無用的字符串生成器。如果你想看看results builder的一些實際用例,我強烈建議你看看我的另一篇文章:How I Created a DSL for Diffable Section Snapshot using Result Builders[1],以及這篇Antoine van der Lee撰寫的:Result builders in Swift explained with code examples[2]。
此外,您還可以查看這個偉大的GitHub repo,它包含大量使用結(jié)果構(gòu)建器構(gòu)建的項目:awesome-function-builders[3]。
7 總結(jié)
我希望這篇文章能讓你很好地了解結(jié)果生成器是如何工作的。如果您對結(jié)果構(gòu)建器的基本概念仍有疑問,您可以在這里[4]獲得完整的示例代碼,然后自己進(jìn)行測試。
參考資料
- [1]How I Created a DSL for Diffable Section Snapshot using Result Builders: https://swiftsenpai.com/swift/result-builders-basics/
- [2]Result builders in Swift explained with code examples: 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]Twitter: https://twitter.com/Lee_Kah_Seng
本文轉(zhuǎn)載自微信公眾號「 Swift 社區(qū)」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系 Swift 社區(qū)公眾號。