Swift 5.7新特性 (上)

9,496 阅读9分钟

译自 www.hackingwithswift.com/articles/24…

更多内容欢迎关注公众号「Swift花园」

Swift 5.7 变化巨大,新特性中包括正则表达式, if let 速记语法,以及围绕 anysome 关键字的一致性改动。

在本文中,我会通过一些示例来介绍这些新特性。

解包可选型的 if let 速记

SE-0345 引入了新的速记语法,可以将可选型展开为同名的阴影变量。以后我们可以像下面这样解包了:

var name: String? = "Linda"

if let name {
    print("Hello, \(name)!"
}

对比之前的写法:

if let name = name {
    print("Hello, \(name)!"
}

if let unwrappedName = name {
    print("Hello, \(unwrappedName)!"
}        

注意:这个变化并不适用于对象内的属性,所以像下面这样的代码无法通过编译:

struct User {
    var name: String
}

let user: User? = User(name: "Linda")

if let user.name {
    print("Welcome, \(user.name)!")
}

多语句闭包类型推断

SE-0326 极大地提高了 Swift 对闭包使用参数和类型推断的能力,这意味着我们现在可以删除许多必须明确指定输入和输出类型的写法。

之前 Swift 处理闭包的书写难免琐碎,但从 Swift 5.7 开始,我们可以编写如下简化的代码:

let scores = [100, 80, 85]

let results = scores.map { score in
    if score >= 85 {
        return "\(score)%: Pass"
    } else {
        return "\(score)%: Fail"
    }
}

在 Swift 5.7 之前则必须像下面这样书写:

let oldResults = scores.map { score -> String in
    if score >= 85 {
        return "\(score)%: Pass"
    } else {
        return "\(score)%: Fail"
    }
}

Clock,Instant 和 Duration

SE-0329 为 Swift 引入了一种新的标准化方式来引用时间和持续时间。它可以拆解为三个主要部分:

  • Clock 代表了一种测量时间流逝的方式。有两个内置时钟:连续时钟在系统处于睡眠状态时也会保持时间递增,而挂起时钟则不会。
  • Instant 代表一个精确的瞬间。
  • Durations 表示两个 Instant 之间经过了多少时间。

这个新特性对许多人来说来联想到的最直接的应用就是新升级的 Task API:它现在可以用比纳秒更合理的术语来指定休眠时长:

try await Task.sleep(until: .now +  .seconds(1), clock: .continuous)

这个新 API 还有一个好处是能够指定容差,使得系统在睡眠截止日期之后能够稍等片刻,以便最大限度地提高电源效率。所以,假如我们想 sleep 至少 1 秒,并且能接受它总共持续 1.5 秒,我们可以这样写:

try await Task.sleep(until: .now + .seconds(1), tolerance: .seconds(0.5), clock: .continuous)

时钟对于测量某些特定的工作也很有用。比如,我们想向用户展示文件导出过程花费了多长时间,可以使用时钟的

measure 闭包:

let clock = ContinuousClock()

let time = clock.measure {
    // complex work here
}

print("Took \(time.components.seconds) seconds")

正则表达式

Swift 5.7 引入了大量与正则表达式相关的改进,这是一整套相互关联的提案,包括:

  • SE-0350 引入了新的 Regex 类型
  • SE-0351 引入了一个用于创建正则表达式的 result builder 驱动的 DSL。
  • SE-0354 引入了使用 /.../ 而不单是 Regex 来共同创建正则表达式的方式。
  • SE-0357 添加了许多新的基于正则表达式的字符串处理算法。

与其他语言和平台相比,正则表达式一直是 Swift 语言一个相当大的痛点。

现在,让我们从简单的例子开始:

let message = "the cat sat on the mat"
print(message.ranges(of: "at"))
print(message.replacing("cat", with: "dog"))
print(message.trimmingPrefix("the "))

它们的真正威力在于也都接受正则表达式:

print(message.ranges(of: /[a-z]at/))
print(message.replacing(/[a-m]at/, with: "dog"))
print(message.trimmingPrefix(/The/.ignoresCase()))

如果您不熟悉正则表达式,下面是几条快速入门:

  • 在第一个正则表达式中,我们要求所有匹配任何小写字母后跟“at”的子字符串的范围,以便找到“cat”、“sat”和“mat”的位置。
  • 在第二个正则表达式中,我们只匹配从“a”到“m”的范围,所以 sat 不会被替换,它会打印“the dog sat on the dog”。
  • 在第三个正则表达式中,我们寻找“The”,但将正则表达式修改为不区分大小写,以便匹配“the”、“THE”等。

注意这些正则表达式是如何使用正则表达式字面量来生成的 —— 以 / 开始和结束。

除了正则表达式字面量,Swift 还提供了专门的 Regex 类型:

do {
    let atSearch = try Regex("[a-z]at")
    print(message.ranges(of: atSearch))
} catch {
    print("Failed to create regex")
}

这里两种方式有一个关键区别:当我们使用 Regex 从字符串创建正则表达式时,Swift 必须在运行时解析字符串以找出它应该使用的实际表达式。相比之下,使用正则表达式字面量允许 Swift 在编译时 检查你的正则表达式:它可以验证正则表达式不包含错误,并且还可以准确了解它将包含什么匹配项。

在编译时解析你的正则表达式,确保它们是有效的 —— 牛🍺!

想知道这个差异有多强大,咱们来看下面的代码:

let search1 = /My name is (.+?) and I'm (\d+) years old./
let greeting1 = "My name is Taylor and I'm 26 years old."

if let result = try search1.wholeMatch(in: greeting1) {
    print("Name: \(result.1)")
    print("Age: \(result.2)")
}

这会创建一个正则表达式来查找某些文本中的两个特定值,如果找到它们都会打印它们。但请注意 result 元组如何将其匹配项引用为 .1.2,因为 Swift 知道将发生哪些匹配项。 (.0 将返回整个匹配的字符串。)

事实上,正则表达式还允许我们命名匹配项,这些匹配项会流向生成的匹配元组:

let search2 = /My name is (?<name>.+?) and I'm (?<age>\d+) years old./
let greeting2 = "My name is Taylor and I'm 26 years old."

if let result = try search2.wholeMatch(in: greeting2) {
    print("Name: \(result.name)")
    print("Age: \(result.age)")
}

这种安全性对于从字符串创建的正则表达式是不可能的。

但 Swift 更进一步,你还可以从类似于 SwiftUI 代码的 DSL 语言创建正则表达式。

例如,如果我们想匹配 “我的名字是 Taylor,我 26 岁” 的文本,我们可以写一个这样的正则表达式:

let search3 = Regex {
    "My name is "

    Capture {
        OneOrMore(.word)
    }

    " and I'm "

    Capture {
        OneOrMore(.digit)
    }

    " years old."
}

更棒的是,这种 DSL 方法能够对其找到的匹配项应用转换,如果我们使用 TryCapture 而不是 Capture,在捕获失败或有错误抛出时,Swift 将自动认为整个正则表达式不匹配。因此,在我们的年龄匹配的例子中,我们可以编写以下代码来将年龄字符串转换为整数:

let search4 = Regex {
    "My name is "

    Capture {
        OneOrMore(.word)
    }

    " and I'm "

    TryCapture {
        OneOrMore(.digit)
    } transform: { match in
        Int(match)
    }

    Capture(.digit)

    " years old."
}

你甚至可以使用具有特定类型的变量将命名匹配组合在一起,如下所示:

let nameRef = Reference(Substring.self)
let ageRef = Reference(Int.self)

let search5 = Regex {
    "My name is "

    Capture(as: nameRef) {
        OneOrMore(.word)
    }

    " and I'm "

    TryCapture(as: ageRef) {
        OneOrMore(.digit)
    } transform: { match in
        Int(match)
    }

    Capture(.digit)

    " years old."
}

if let result = greeting.firstMatch(of: search5) {
    print("Name: \(result[nameRef])")
    print("Age: \(result[ageRef])")
}

在这三个选项中,我怀疑正则表达式文字会得到最广泛的使用。尽管在 Swift 6 发布之前,默认情况下对它们的支持将被禁用。你可以把 “-Xfrontend -enable-bare-slash-regex” 添加到 Xcode 中的 Swift Flags 设置以启用这个语法特性。

基于默认表达式的类型推断

SE-0347 扩展了 Swift 使用泛型参数类型的默认值的能力。这个特性似乎相当小众,但确实重要:如果你有一个泛型类型或函数,现在可以为默认表达式提供一个具体类型。

例如,我们可能有一个函数,它从任意类型的序列中返回 count 个随机项:

func drawLotto1<T: Sequence>(from options: T, count: Int = 7) -> [T.Element] {
    Array(options.shuffled().prefix(count))
}

这允许我们使用任何类型的序列来运行函数,例如字符串数组或者整数范围:

print(drawLotto1(from: 1...49))
print(drawLotto1(from: ["Jenny", "Trixie", "Cynthia"], count: 2))

SE-0347 允许我们为函数中的 T 参数提供一个具体类型作为默认值,同时允许我们保持使用字符串数组或任何其他序列类型的灵活性:

func drawLotto2<T: Sequence>(from options: T = 1...49, count: Int = 7) -> [T.Element] {
    Array(options.shuffled().prefix(count))
}

这样一来我们既可以使用自定义序列调用函数,也可以让默认值接管:

print(drawLotto2(from: ["Jenny", "Trixie", "Cynthia"], count: 2))
print(drawLotto2())

顶级代码的并发

SE-0343 升级了 Swift 对顶级代码的支持——想想 macOS 命令行工具项目中的 main.swift —— 以便它支持开箱即用的并发。这个变化看起来微不足道,但为了支持它需要相当多的工作。

实践上,这个变化意味着我们可以将这样的代码直接写入 main.swift 文件:

let url = URL(string: "https://hws.dev/readings.json")!
let (data, _) = try await URLSession.shared.data(from: url)
let readings = try JSONDecoder().decode([Double].self, from: data)
print("Found \(readings.count) temperature readings")

在这个变化以前,我们必须创建一个具有异步 main() 方法的 @main 结构。因此说这个变化是一个不小的改进。

不透明参数声明

SE-0341 解锁了在使用更简单泛型的地方对参数声明使用 some 的能力。

举个例子,如果我们想编写一个检查数组是否排序的函数,Swift 5.7 及更高版本允许我们这样写:

func isSorted(array: [some Comparable]) -> Bool {
    array == array.sorted()
}

[some Comparable] 参数类型意味着此函数适用于包含某种类型的元素的数组,该类型遵循 Comparable 协议,这是等效通用代码的语法糖:

func isSortedOld<T: Comparable>(array: [T]) -> Bool {
    array == array.sorted()
}

当然,我们也可以写更长的约束扩展:

extension Array where Element: Comparable {
    func isSorted() -> Bool {
        self == self.sorted()
    }
}

这种简化的泛型语法确实意味着我们不再有能力为我们的类型添加更复杂的约束,因为合成的泛型参数没有特定的名称。

重要提示: 你可以在显式泛型参数和这种新的更简单语法之间切换,而不会破坏 API。

结构化的不透明结果类型

SE-0328 拓宽了不透明结果类型可以使用的范围。

例如,我们现在可以一次返回多个不透明类型:

func showUserDetails() -> (some Equatable, some Equatable) {
    (Text("Username"), Text("@twostraws"))
}

我们还可以返回不透明类型数组:

func createUser() -> [some View] {
    let usernames = ["@frankefoster", "@mikaela__caron", "@museumshuffle"]
    return usernames.map(Text.init)
}

甚至返回一个在调用时本身返回不透明类型的函数:

func createDiceRoll() -> () -> some View {
    return {
        let diceRoll = Int.random(in: 1...6)
        return Text(String(diceRoll))
    }
}

因此,这是 Swift 进化过程中保持一致性的另一个很好的例子。

封面来自 Pauline Loroy on Unsplash

更多内容欢迎关注公众号「Swift花园」