Swift之Result Builder

Result Builder是一個自定義的類型,添加了相關語法,用來以自然地、聲明的方式來創建嵌套的數據,比如鏈表和樹。使用Result Builder的代碼中可以包含原始的Swift語法,比如iffor以方便掌控條件數據或者重復的數據。

如下代碼定義了一個新的類型,用來顯示一行由*和文本組成的數據。

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里面的深層次嵌套括號使人難以理解。namenil的時候使用"World"替換的補充邏輯必須使用??操作符,當使用更復雜的字符串時,這個邏輯會變得更加困難。你完全沒有辦法在你需要的時候使用switch或者for循環來構建圖片的片段。Result Builder能夠讓你重構代碼使其更像正常的 Swift代碼。

通過在類型聲明前添加@resultBuilder特性來定義一個Result Builder。下面的代碼定義了一個叫做DrawingBuilderResult 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上的函數調用,來構造作為函數參數傳進來的值。例如,Swiftcaps(_:)的調用轉化為下面的代碼:

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
}

Swiftif-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的講解。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容