Swift Builders

CI

一个为 Swift 和 Foundation 值类型提供实用 Result Builders 的集合。

动机

在 Swift 中,数组、字典和其他基于集合的类型相对容易构建和修改。

然而,当包含的元素依赖于某些条件或复杂的逻辑时,事情就会变得棘手。一个典型的例子是构建要发送到分析服务的有效负载。结果代码可能如下所示:

func checkoutAnalyticsEvent(didSucceed: Bool, purchaseAmount: Decimal, userId: String?) -> [String: String] {
    var event: [String: String] = [:]
    event["success"] = didSucceed ? "true" : "false"
    if purchaseAmount > 0 {
        event["amount"] = purchaseAmount.formatted(.number.precision(.fractionLength(2)))
    } else {
        event["isFree"] = "true"
    }
    if let userId = userId {
        event["userId"] = userId
    } else {
        event["isGuest"] = "true"
    }
    return event
}

这还不错,但绝对没有人们期望的那么 Swifty。

我们正在将命令式代码洒在我们应该只是有效负载描述的地方。这不仅使一眼就能推断代码变得更加困难,而且还为意外的修改留下了太多的余地。

幸运的是,有一种更好的方法...

开始使用

Swift Builders 允许为 Swift 和 Foundation 中的大多数 Collection 类型使用 result builder 语法。

例如,通过利用 Dictionary.build,我们上面的用例变为:

import Builders

func checkoutAnalyticsEvent(didSucceed: Bool, purchaseAmount: Decimal, userId: String?) -> [String: String] {
    return [String: String].build {
        ["success": didSucceed ? "true" : "false"]
        if purchaseAmount > 0 {
            ["amount": purchaseAmount.formatted(.number.precision(.fractionLength(2)))]
        } else {
            ["isFree": "true"]
        }
        if let userId = userId {
            ["userId": userId]
        } else {
            ["isGuest": "true"]
        }
    }
}

我们甚至可以使用 @DictionaryBuilder 属性注释我们的函数,使函数体像 builder 本身一样工作(想想 @ViewBuilder

import Builders

@DictionaryBuilder<String, String>
func checkoutAnalyticsEvent(didSucceed: Bool, purchaseAmount: Decimal, userId: String?) -> [String: String] {
    ["success": didSucceed ? "true" : "false"]
    if purchaseAmount > 0 {
        ["amount": purchaseAmount.formatted(.number.precision(.fractionLength(2)))]
    } else {
        ["isFree": "true"]
    }
    if let userId = userId {
        ["userId": userId]
    } else {
        ["isGuest": "true"]
    }
}

这只是将 result builders 应用于 Swift 本地类型的强大功能的一个小演示。

该库开箱即用地提供了各种 builders:

基准测试

MacBook Pro (14-inch, 2021)
Apple M1 Pro (10 cores, 8 performance and 2 efficiency)
32 GB Memory

$ swift run -c release Benchmarks

name                              time        std        iterations
-------------------------------------------------------------------
Array<Any>.build                  1833.000 ns ±   7.60 %     757726
Array<Int>.build                   542.000 ns ±  15.49 %    1000000
Array<Int?>.build                  709.000 ns ±   9.51 %    1000000
ArraySlice<Any>.build             2750.000 ns ±   5.28 %     511759
ArraySlice<Int>.build              875.000 ns ±   8.40 %    1000000
ArraySlice<Int?>.build            1167.000 ns ±  13.55 %    1000000
ContiguousArray<Any>.build        1917.000 ns ±  12.37 %     729365
ContiguousArray<Int>.build         542.000 ns ±  23.24 %    1000000
ContiguousArray<Int?>.build        750.000 ns ±  13.97 %    1000000
Data.build                         875.000 ns ±  13.55 %    1000000
Dictionary<String, Any>.build     4209.000 ns ±   6.26 %     328025
Dictionary<String, Double>.build  2459.000 ns ±  11.92 %     562007
Dictionary<String, Double?>.build 2583.000 ns ±   5.51 %     526636
Set<Any>.build                    6333.000 ns ±  10.30 %     228224
Set<Int>.build                     750.000 ns ±  11.22 %    1000000
Set<Int?>.build                   1292.000 ns ±  11.42 %    1000000
Slice<Array<Any>>.build           2209.000 ns ±   4.95 %     629537
Slice<Array<Int>>.build            584.000 ns ±  17.10 %    1000000
Slice<Array<Int?>>.build           917.000 ns ±   8.49 %    1000000
String.build                       500.000 ns ±   8.91 %    1000000
String.UnicodeScalarView.build    3958.000 ns ±   3.04 %     351918
String.UTF8View.build              542.000 ns ±  10.33 %    1000000
Substring.build                   1709.000 ns ±   4.41 %     810685
Substring.UnicodeScalarView.build 5084.000 ns ±   3.19 %     274560
Substring.UTF8View.build          1333.000 ns ±   5.89 %    1000000