Swift 5.7 新特性
July 21st, 2022

简化的可选绑定(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

Subscribe to zzzwco
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.
More from zzzwco

Skeleton

Skeleton

Skeleton