Result Builder是一個自定義的類型,添加了相關語法,用來以自然地、聲明的方式來創建嵌套的數據,比如鏈表和樹。使用Result Builder的代碼中可以包含原始的Swift語法,比如if
和for
以方便掌控條件數據或者重復的數據。
如下代碼定義了一個新的類型,用來顯示一行由*
和文本組成的數據。
protocol Drawable {
func draw() -> String
}
struct Line: Drawable {
func draw() -> String {
return elements.map { $0.draw() }.joined(separator: "")
}
var elements: [Drawable]
}
struct Space: Drawable {
func draw() -> String {
return " "
}
}
struct Text: Drawable {
func draw() -> String {
return content
}
init(_ content: String) {
self.content = content
}
var content: String
}
struct Stars: Drawable {
func draw() -> String {
return String(repeating: "*", count: length)
}
var length: Int
}
struct AllCaps: Drawable {
func draw() -> String {
content.draw().uppercased()
}
var content: Drawable
}
Drawable
協議定義了可以被繪制的組件的要求,比如直線和形狀:組件類型必須實現協議要求的draw()
方法。結構體Line
代表一條直線的繪制,并且他是大多數可繪制類型的最高層次的容器。為了繪制Line
,結構體調用各個組件的draw()
方法,然后把各個結果字符串連接成一個字符串。Text
結構體包裝了一個字符串并使其成為繪制的一部分。AllCaps
結構體包裝并修改另一個可以繪制的實例---把其中的字母全部變為大寫。
利用這些類型,通過調用其初始化器很容易繪制一幅圖:
let name: String? = "Ravi Patel"
let manualDrawing = Line(elements: [
Stars(length: 3),
Text("Hello"),
Space(),
AllCaps(content: Text((name ?? "World") + "!")),
Stars(length: 2)
])
print(manualDrawing.draw())
// Prints: ***Hello RAVI PATEL!**
代碼可以正常運行,但是略顯笨拙。AllCaps
里面的深層次嵌套括號使人難以理解。name
是nil
的時候使用"World"替換的補充邏輯必須使用??
操作符,當使用更復雜的字符串時,這個邏輯會變得更加困難。你完全沒有辦法在你需要的時候使用switch
或者for
循環來構建圖片的片段。Result Builder能夠讓你重構代碼使其更像正常的 Swift代碼。
通過在類型聲明前添加@resultBuilder
特性來定義一個Result Builder。下面的代碼定義了一個叫做DrawingBuilder
的Result Builder,它可以讓你使用聲明式語法來描述繪制過程。
@resultBuilder
struct DrawingBuilder {
static func buildBlock(_ components: Drawable...) -> Drawable {
return Line(elements: components)
}
static func buildEither(first component: Drawable) -> Drawable {
return component
}
static func buildEither(second component: Drawable) -> Drawable {
return component
}
}
DrawingBuilder
定義了三個方法,實現了Result Builder的部分語法規則。buildBlock(_:)
方法為在一個代碼塊中實現繪制一系列直線提供了支持---把多個可以繪制的組件組合到了一個Line
中。buildEither(first:)
和buildEither(second:)
為if-else
提供了支持。
可以把@DrawingBuilder
應用在函數參數上,并把傳入函數的閉包轉換成Result Builder根據閉包創建的值。代碼如下:
func draw(@DrawingBuilder content: () -> Drawable ) -> Drawable {
return content()
}
func caps(@DrawingBuilder content: () -> Drawable ) -> Drawable {
return AllCaps(content: content())
}
func makeGreeting(for name: String? = nil) -> Drawable {
let greeting = draw {
Stars(length: 3)
Text("Hello")
Space()
caps {
if let name = name {
Text(name + "!")
} else {
Text("World!")
}
}
Stars(length: 2)
}
return greeting
}
let genericGreeting = makeGreeting()
print(genericGreeting.draw())
// Prints ***Hello WORLD!**
makeGreeting(for:)
需要一個name
參數并使用它構造一個個性化的問候。drwa(_:)
和caps(_:)
函數都需要一個標記了@DrawingBuilder
的閉包作為參數。當你調用這些函數,你使用了DrawingBuilder
定義的特殊語法。Swift把一幅繪制內容的聲明式描述轉化為一系列的DrawingBuilder
上的函數調用,來構造作為函數參數傳進來的值。例如,Swift把caps(_:)
的調用轉化為下面的代碼:
let capsDrawing = caps {
let partialDrawing:Drawable
if let name = name {
let text = Text(name + "!")
partialDrawing = DrawingBuilder.buildEither(first: text)
} else {
let text = Text("World!")
partialDrawing = DrawingBuilder.buildEither(second: text)
}
return partialDrawing
}
Swift把if-else
的代碼塊轉化為buildEither(first:)
和buildEither(second:)
函數的調用。盡管在你的代碼里面們不會調用這些函數,展示出來的轉換結果使得弄清楚當你使用DrawingBuilder
語法時Swift是如何轉換你的代碼這件事變得比較容易。
為特殊繪制語法添加for
循環的支持,只需要在DrawingBuilder
中添加buildArray(_:)
方法:
extension DrawingBuilder {
static func buildArray(_ components: [Drawable]) -> Drawable {
return Line(elements: components)
}
}
let manyStars = draw {
Text("Stars:")
for length in 1...3 {
Space()
Stars(length: length)
}
}
print(manyStars.draw())
// Prints Stars: * ** ***
上面的代碼中,for
循環創建了一批可以繪制的組件,buildArray(_:)
方法把這一批組件轉化為一個Line
組件。
想要了解更多關于Swift把創建器語法轉化為創建器類型方法的調用的原理,參考Swift特性下面關于resultBuilder
的講解。