Swift 5.4 有什么新功能?

文章目录
  1. 1. 导言
  2. 2. 改进了隐式成员语法
  3. 3. 函数中的多变量参数
  4. 4. 结果生成器
  5. 5. 本地函数现在支持重载
  6. 6. 软件包现在可以声明可执行目标
  7. 7. 自己尝试

导言

多个变量参数,改进隐式成员语法,结果构建器等。

Swift 5.4带来了一些巨大的编译改进,包括更好地完成带错误的表达式中的代码,以及增量编译的大提速。不过,它也增加了一些重要的新特性和改进,让我们在这里深入了解一下……

  • 小贴士:

    如果你想自己尝试代码样本,也可以下载这个作为Xcode Playground。

改进了隐式成员语法

SE-0287改进了Swift使用隐式成员表达式的能力,所以你可以制作它们的链子,而不是只支持一个单一的静态成员。

Swift一直以来都有能力使用隐式成员语法来处理简单的表达式,例如,如果你想在SwiftUI中给一些文本着色,你可以使用.red而不是Color.red。

`struct ContentView1: View {
    var body: some View {
        Text("You're not my supervisor!")
            .foregroundColor(.red)
    }
}
`

在Swift 5.4之前,这在更复杂的表达式中是行不通的。例如,如果你想让你的红色略微透明,你就需要这样写。

`struct ContentView2: View {
    var body: some View {
        Text("You're not my supervisor!")
            .foregroundColor(Color.red.opacity(0.5))
    }
}
`

从Swift 5.4开始,编译器能够理解多个链式成员,这意味着可以推断出Color类型。

`struct ContentView3: View {
    var body: some View {
        Text("You're not my supervisor!")
            .foregroundColor(.red.opacity(0.5))
    }
}
`

函数中的多变量参数

SE-0284引入了让函数、下标和初始化器使用多个变量参数的能力,只要变量参数后面的所有参数都有标签。在Swift 5.4之前,这种情况下只能有一个变量参数。

所以,有了这个改进,我们可以写一个函数,接受一个变量参数,存储足球比赛中进球的次数,再加上第二个变量参数,打出进球球员的名字。

`func summarizeGoals(times: Int..., players: String...) {
    let joinedNames = ListFormatter.localizedString(byJoining: players)
    let joinedTimes = ListFormatter.localizedString(byJoining: times.map(String.init))

    print("\(times.count) goals where scored by \(joinedNames) at the follow minutes: \(joinedTimes)")
}
`

要调用该函数,提供两组值作为变量参数,确保第一个变量之后的所有参数都有标签。

`summarizeGoals(times: 18, 33, 55, 90, players: "Dani", "Jamie", "Roy")
`

结果生成器

函数构建器在Swift 5.1中非正式地出现了,但在Swift 5.4之前,它们作为SE-0289正式通过了Swift进化提案过程,以便进行讨论和完善。作为这个过程的一部分,它们被重新命名为结果构建器以更好地反映它们的实际目的,甚至获得了一些新的功能。

首先是最重要的部分:结果构建器允许我们通过在我们选择的序列中一步步地创造一个新的价值。它们为SwiftUI的视图创建系统的很大一部分提供了动力,所以当我们拥有一个内部有各种视图的VStack时,Swift会默默地将它们归为一个内部的TupleView类型,这样它们就可以作为VStack的一个子代来存储–它将一个视图序列变成了一个视图。

结果构建器应该有自己的详细文章,但我至少想给你一些小的代码示例,这样你就可以看到它们的运作。

下面是一个返回单个字符串的函数。

`func makeSentence1() -> String {
    "Why settle for a Duke when you can have a Prince?"
}

print(makeSentence1())
`

这很好用,但如果有几个字符串我们想连接在一起呢?就像SwiftUI一样,我们可能想把它们都单独提供,然后让Swift来解决。

`// This is invalid Swift, and will not compile.
// func makeSentence2() -> String {
//     "Why settle for a Duke"
//     "when you can have"
//     "a Prince?"
// }
`

就其本身而言,这段代码是行不通的,因为Swift不再理解我们的意思。然而,我们可以创建一个结果生成器,它可以理解如何使用我们想要的任何转换将几个字符串转换为一个字符串,就像这样。

`@resultBuilder
struct SimpleStringBuilder {
    static func buildBlock(_ parts: String...) -> String {
        parts.joined(separator: "\n")
    }
}
`

尽管这只是少量的代码,但有很多需要解压。

  • @resultBuilder属性告诉SwiftUI以下类型应该被视为结果构建器。

    以前这种行为是通过@_functionBuilder实现的,它有一个下划线来表明这不是为一般使用而设计的。

  • 每个结果构建器都必须提供至少一个名为buildBlock()的静态方法,该方法应该接收某种数据并对其进行转换。

    上面的例子是接收零个或多个字符串,将它们连接起来,然后以单个字符串的形式发送回来。

  • 最终的结果是,我们的SimpleStringBuilder结构变成了一个结果构建器,这意味着我们可以在任何需要它的字符串连接能力的地方使用@SimpleStringBuilder。

没有什么可以阻止我们直接使用SimpleStringBuilder.buildBlock(),就像这样。

`let joined = SimpleStringBuilder.buildBlock(
    "Why settle for a Duke",
    "when you can have",
    "a Prince?"
)

print(joined)
`

然而,由于我们在SimpleStringBuilder结构中使用了@resultBuilder注解,我们也可以将其应用到函数中,就像这样。

`@SimpleStringBuilder func makeSentence3() -> String {
    "Why settle for a Duke"
    "when you can have"
    "a Prince?"
}

print(makeSentence3())
`

请注意,我们不再需要每个字符串末尾的逗号--@resultBuilder通过使用SimpleStringBuilder自动将makeSentence()中的每个语句转换为单个字符串。

在实践中,结果生成器能够做得更多,通过向你的生成器类型添加更多的方法来完成。例如,我们可以通过添加两个额外的方法来为我们的SimpleStringBuilder添加if/else支持,这两个方法描述了我们要如何转换数据。在我们的代码中,我们根本不想转换我们的字符串,所以我们可以直接把它们送回去。

`@resultBuilder
struct ConditionalStringBuilder {
    static func buildBlock(_ parts: String...) -> String {
        parts.joined(separator: "\n")
    }

    static func buildEither(first component: String) -> String {
        return component
    }

    static func buildEither(second component: String) -> String {
        return component
    }
}
`

我知道,看起来我们几乎没有做任何工作,但现在我们的函数能够使用条件。

`@ConditionalStringBuilder func makeSentence4() -> String {
    "Why settle for a Duke"
    "when you can have"

    if Bool.random() {
        "a Prince?"
    } else {
        "a King?"
    }
}

print(makeSentence4())
`

同样,我们也可以通过在构建器类型中添加 buildArray() 方法来添加对循环的支持。

`@resultBuilder
struct ComplexStringBuilder {
    static func buildBlock(_ parts: String...) -> String {
        parts.joined(separator: "\n")
    }

    static func buildEither(first component: String) -> String {
        return component
    }

    static func buildEither(second component: String) -> String {
        return component
    }

    static func buildArray(_ components: [String]) -> String {
        components.joined(separator: "\n")
    }
}
`

现在我们可以使用for循环。

`@ComplexStringBuilder func countDown() -> String {
    for i in (0...10).reversed() {
        "\(i)…"
    }

    "Lift off!"
}

print(countDown())
`

这感觉几乎就像魔法一样,因为结果构建器系统几乎为我们做了所有的工作,尽管我们的例子已经相当简单,但我希望你能感受到结果构建器给Swift带来的非凡力量。

值得补充的是,Swift 5.4扩展了结果构建器系统,支持属性被放置在存储的属性上,这将自动调整结构的隐式成员初始化器来应用结果构建器。

这对于使用结果构建器的自定义SwiftUI视图特别有帮助,比如这个视图。

`struct CustomVStack<Content: View>: View {
    @ViewBuilder let content: Content

    var body: some View {
        VStack {
            // custom functionality here
            content
        }
    }
}
`

如果你想看到更多高级的、实际操作的结果构建器的例子,你应该查看GitHub上的Awesome Function Builders仓库。

本地函数现在支持重载

SR-10069要求能够在本地上下文中重载函数,这实际上意味着嵌套函数现在可以重载,这样Swift就可以根据使用的类型来选择运行哪个函数。

例如,如果我们想制作一些简单的 cookie,我们可能会写这样的代码。

`struct Butter { }
struct Flour { }
struct Sugar { }

func makeCookies() {
    func add(item: Butter) {
        print("Adding butter…")
    }

    func add(item: Flour) {
        print("Adding flour…")
    }

    func add(item: Sugar) {
        print("Adding sugar…")
    }

    add(item: Butter())
    add(item: Flour())
    add(item: Sugar())
}
`

在Swift 5.4之前,三个add()方法只有在没有嵌套在makeCookies()里面的情况下才能被重载,但从Swift 5.4开始,这种情况下也支持函数重载。

现在支持本地变量的属性包装器了 属性包装器最早是在Swift 5.1中引入的,作为一种以简单、可重用的方式为属性附加额外功能的方式,但在Swift 5.4中,它们的行为得到了扩展,支持在函数中作为局部变量使用。

例如,我们可以创建一个属性包装器,确保其值永远不会低于零。

`@propertyWrapper struct NonNegative<T: Numeric & Comparable> {
    var value: T

    var wrappedValue: T {
        get { value }

        set {
            if newValue < 0 {
                value = 0
            } else {
                value = newValue
            }
        }
    }

    init(wrappedValue: T) {
        if wrappedValue < 0 {
            self.value = 0
        } else {
            self.value = wrappedValue
        }
    }
}
`

而从Swift 5.4开始,我们可以在一个常规函数里面使用这个属性包装器,而不仅仅是附加到一个属性上。例如,我们可以写一个游戏,在这个游戏中,我们的玩家可以获得或失去分数,但他们的分数绝对不能低于0。

`func playGame() {
    @NonNegative var score = 0

    // player was correct
    score += 4

    // player was correct again
    score += 8

    // player got one wrong
    score -= 15

    // player got another one wrong
    score -= 16

    print(score)
}
`

软件包现在可以声明可执行目标

SE-0294 为使用 Swift 包管理器的应用程序增加了一个新的目标选项,允许我们明确声明一个可执行目标。

这对于想要使用 SE-0281(使用 @main 来标记你的程序的入口点)的人来说特别重要,因为它与 Swift 包管理器玩得并不好–它总是会寻找 main.swift 文件。

通过这次修改,我们现在可以删除main.swift而使用@main来代替。注意:你必须在你的Package.swift文件中指定// swift-tools-version:5.4才能获得这个新功能。

自己尝试

Swift 5.4可以通过Xcode 12.5获得,Xcode 12.5于2021年2月1日进入测试版。如果你还没有升级到macOS Big Sur,你将无法安装Xcode 12.5,所以你应该从https://swift.org/download/ 下载一个Swift 5.4工具链–你可以将其安装到Xcode 12.4和更早的版本中。

你最期待Swift 5.4的哪些功能?