Swift 5.7 新特性

Published on
Authors
Table of Contents

Xcode 14b3 Swift 5.7

简化的可选绑定(SE-0345

对可选类型解包时无需显式绑定,写法更简洁:

let s1: String? = "s1"

if let s1 {
  print(s1)
}

guard let s1 else {
  exit(0)
}
print(s1)

更强大的类型推断

默认表达式的类型推断(SE-0347

Swift 现在支持给泛型参数赋予默认值,并且能根据上下文推断泛型参数的具体类型。

func compute<C: Collection>(_ values: C = [0, 1, 2]) { }

compute([1, 2, 3]) // [Int]
compute(["a", "b", "c"]) // [String]
compute([1, "2", {}]) // [Any]

多语句闭包的类型推断(SE-0326

以前的多语句闭包必须写明参数和返回值类型:

let _ = [-1, 0, 1].map { v -> String in
  if v < 0 {
    return "negative"
  } else if v > 0 {
    return "positive"
  } else {
    return "zero"
  }
}

现在,编译器会自动推断:

let _ = [-1, 0, 1].map {
  if $0 < 0 {
    return "negative"
  } else if $0 > 0 {
    return "positive"
  } else {
    return "zero"
  }
}

更灵活的正则表达式

正则类型(SE-0350

Swift 5.7 新增了正则类型 Regex<Output>,用于便捷地构建正则表达式。

let s = "Stay hungry, stay foolish."
let regex1 = try! Regex("[Ss]tay")
let matches1 = s.matches(of: regex1)
for match in matches1 {
  let l = match.range.lowerBound
  let r = match.range.upperBound
  printLog(s[l..<r])
}

正则构造器 DSL(SE-0351

正则表达式简洁有力,但难以书写。因此 Swift 提供了 DSL 供我们使用,便于方便地书写正则表达式。比如下面示例代码中的 OneOrMore(.word) 表达的意思和 /[A-Za-z0-9]+/ 是一样的。

import RegexBuilder

let regex2 = Regex {
  "Stay "
  Capture {
    OneOrMore(.word) // matches2.1
  }
  ", stay "
  Capture {
    OneOrMore(.word) // matches2.2
  }
  "."
}
if let matches2 = try regex2.wholeMatch(in: s) {
  // matches2.0 是整个匹配的字符串
  printLog(matches2.0, matches2.1, matches2.2)
}

Regex 还支持使用别名和下标来获取匹配结果:

let ref1 = Reference(Substring.self)
let ref2 = Reference(Substring.self)
let regex3 = Regex {
  "Stay "
  Capture(as: ref1) { // res[ref1]
    OneOrMore(.word)
  }
  ", stay "
  Capture(as: ref2) { // res[ref2]
    OneOrMore(.word)
  }
  "."
}
if let matches3 = try regex3.wholeMatch(in: s) {
  printLog(matches3[ref1], matches3[ref2])
}

另外,RegexBuilder 中的 buildPartialBlock 实现了基于结果生成器的重载。这是 SwiftUI 喜闻乐见的,因为此前的 ViewBuilder ****最多只能从 10 个子 view 构建,但 buildPartialBlock 可以突破这个限制。这和 reduce 函数有点类似,在前一个生成的结果基础上,继续累积新的值。

更多相关 API 请查看:RegexBuilder

正则字面量(SE-0354

Swift 支持使用字面量直接构建正则表达式,构建方式非常简单,只需要将表达式置于两个 / 之间:

let regex4 = /[Ss]tay/
let matches4 = s.matches(of: regex4)
for match in matches4 {
  let l = match.range.lowerBound
  let r = match.range.upperBound
  printLog(s[l..<r])
}

字面量构建的正则表达式同样支持使用别名对匹配结果进行引用:

let regex5 = /Stay (?<s1>.+), stay (?<s2>[A-Za-z0-9]+)./
if let matches5 = try regex5.wholeMatch(in: s) {
  printLog(matches5.s1, matches5.s2)
}

值得注意的是,基于字符串构建的 Regex 类型必须在运行时才能对该字符串进行正确解析。而正则字面量在编译期就能被编译器诊断出错误,这也是我们应该优先使用正则字面量的原因。下面是 Regex 类型和字面量结合使用的示例:

let regex6 = Regex {
  "Stay"
  Capture { // 空格使用使用\\转义
    /\\ .+/
  }
  ", stay"
  Capture {
    /\\ [A-Za-z0-9]+/
  }
  "."
}
if let matches6 = try regex6.wholeMatch(in: s) {
  printLog(matches6.1, matches6.2)
}

基于正则的字符串处理算法(SE-0357

除了上面提到的 matches(of:)wholeMatch(in:),Swift 中的集合类型许多原有的方法也提供了对 Regex 的支持。比如:

printLog(s.replacing(/[Ss]tay/, with: "be"))
printLog(s.contains(/\\ foo.+/))

阐明了非隔离异步函数的执行(SE-0338

在此之前,在 g 中调用 f 时,f 可能会 actor 上执行并造成长时间的阻塞。

而现在所有的非隔离异步函数的执行,都会在全局的协作并发池上执行。当然,从 actor 切换至全局并发池执行时,程序依然会进行 Sendable 检查。比如,在 g 中调用 f 时,如果 c 没有实现 Sendable,编译器会发出警告。

class C { }

// 总是在全局的协作并发池上执行
func f(_: C) async { }

actor A {
  func g(c: C) async {
		// 总是在 actor 上执行
    print("on the actor")

    await f(c)
  }
}

新的时间 API (SE-0329

Swift 5.7 提供了一种新的标准化时间组件,由以下三部分组成:

  • Clock:基于 Clock 协议实现,是一种计算时间的机制,定义了现在以及将来某个指定的时间点唤醒工作的方式。
  • Instant:基于 InstantProtocol 协议实现,表示某个时间点。
  • Duration:基于 DurationProtocol 协议实现,表示两个时间点之间的间隔。

Clock 协议定义如下,其中的关联类型 Instant 遵循 InstantProtocol 协议,而另一个关联类型 DurationInstantProtocol 协议中的 Duration: DurationProtocol 的类型保持一致:

@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
public protocol Clock : Sendable {

    associatedtype Duration where Self.Duration == Self.Instant.Duration

    associatedtype Instant : InstantProtocol

    var now: Self.Instant { get }

    var minimumResolution: Self.Duration { get }

    func sleep(until deadline: Self.Instant, tolerance: Self.Instant.Duration?) async throws
}

系统内置了两种 Clock:

  • ContinuousClock:系统睡眠时,仍能计时。
  • SuspendingClock:系统睡眠时,停止计时。

两种 Clock 使用方式一样,这里以 ContinuousClock 为例。比如用来计算某个同步操作的耗时:

let clock = ContinuousClock()
let elapsed = clock.measure {
  for _ in 0..<999999 {}
}
printLog("Loop duration: \\(elapsed)")

Clock 还能用来计算异步事件的耗时,Task 也新增了对 Clock 的支持:

func delayWork() async throws {
  // tolerance 为容差,默认为 nil
  // 这里表示任务会睡眠 0.5 至 1 秒
  let elapsed = try await clock.measure {
    try await Task.sleep(until: .now + .seconds(0.5), tolerance: .seconds(0.5), clock: .continuous)
  }
  printLog("Sleep duration: \\(elapsed)")
  printLog("Time is up, keep working...")
}

try await delayWork()

如果任务在睡眠时间结束前就结束了,会抛出 CancellationError 类型的错误。

不透明类型增强了使用范围(SE-0341

此前,不透明类型只能用于返回值。现在,我们还可以将其用于属性、下标、函数参数以及结构化的返回类型(元组、数组、闭包等)。

func tuple(_ v1: some View, _ v2: some View) -> (some View, some View) {
  (v1, v2)
}

主要关联类型以及轻量级同类型要求(SE-0346

协议支持多个关联类型,使用尖括号声明(类似泛型写法)的则是主要关联类型。

我们来看看 Collection 协议最新的定义:

public protocol Collection<Element> : Sequence {

  associatedtype Element
  associatedtype Iterator = IndexingIterator<Self>
  ...
}

这里的 Element,就是主要关联类型。借助这一特性以及增强了使用范围的不透明类型,在使用具有主要关联类型的协议时,写法可以更优雅。比如下面这个用于比较两个集合的函数:

func compare<C1: Collection, C2: Collection>(_ c1: C1, _ c2: C2) -> Bool
where C1.Element == C2.Element, C1.Element: Equatable { }

可以写得更简洁易读:

func compare<E: Equatable>(_ c1: some Collection<E>, _ c2: some Collection<E>) -> Bool { }

实际上,以前类似的泛型写法 T where T: P, T.E: V 一般都可以简写为 some P<V>

关于存在类型的改进

所有协议都可以作为存在类型使用(SE-0309

Swift 5.6 之前,我么经常遇到协议相关的编译错误:

Protocol can only be used as a generic constraint because it has 'Self' or associated type requirements

通常我们的解决方法是将协议作为泛型约束来解决,Swift 5.6 为此引入了存在类型(一个能够容纳任意遵循某个协议的具体类型的容器类型),并新增了 any 关键字来进行标记。这一特性在 Swift 5.7 中全面解锁,所有的协议都可以使用 any 关键字来进行修饰。

值得注意的是,存在类型会导致性能损耗,any 关键字的主要作用其实是为了提醒我们它带来的潜在副作用,因此我们应该尽量避免使用它,除非你真的需要一个动态的类型。

隐式打开的存在的类型(SE-0352

前面我们提到存在类型是一种容器类型,它只有在运行时才能将容器打开获取到内部的具体类型。这会导致如下的代码报错:

protocol P {
  associatedtype A
  func getA() -> A
}

func takeP<T: P>(_ value: T) { }

func test(p: any P) {
  // error: protocol 'P' as a type cannot conform to itself
  takeP(p)
}

因为泛型约束,takeP 在入参时需要传入一个实现协议 P 的具体类型。而 test 中的 p 是存在类型,它是一个容器类型,其内部的具体类型可以动态改变,并且只有在运行时才能获取到真正的具体类型。所以,我们会看到如上的编译错误。

但现在这个错误不存在了,Swift 赋予了存在类型隐式打开的特性。在 test 中将 p 传入 takeP 时,p 容器内部的具体类型会被自动取出,然后被传递至 takeP 函数。这个自动拆箱的过程,有点类似可选类型中的隐式解包。

受约束的存在类型(SE-0353

具有主要关联类型的协议可以用于存在类型,通过主要关联类型对其进行约束。

比如我们将某个包含整数的集合转换称数组类型:

func mapNumbers(_ c: any Collection<Int>) -> [Int] {
  c.map { $0 }
}

分布式 actor

分布式 actor 主要用于服务端,有兴趣的读者可以参考:SE-0336SE-0344


本文仅是抛砖引玉,关于 Swift 5.7 更详细的变更请参考: Swift/CHANGELOG.md

文中涉及源码参考:Source code

twitterDiscuss on Twitter