Swift 5.7 新特性
- Published on
- Authors
- Name
- zzzwco
- @zzzwco
Table of Contents
- 简化的可选绑定(SE-0345)
- 更强大的类型推断
- 默认表达式的类型推断(SE-0347)
- 多语句闭包的类型推断(SE-0326)
- 更灵活的正则表达式
- 正则类型(SE-0350)
- 正则构造器 DSL(SE-0351)
- 正则字面量(SE-0354)
- 基于正则的字符串处理算法(SE-0357)
- 阐明了非隔离异步函数的执行(SE-0338)
- 新的时间 API (SE-0329)
- 不透明类型增强了使用范围(SE-0341)
- 主要关联类型以及轻量级同类型要求(SE-0346)
- 关于存在类型的改进
- 所有协议都可以作为存在类型使用(SE-0309)
- 隐式打开的存在的类型(SE-0352)
- 受约束的存在类型(SE-0353)
- 分布式 actor
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])
}
SE-0351)
正则构造器 DSL(正则表达式简洁有力,但难以书写。因此 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)
}
}
SE-0329)
新的时间 API (Swift 5.7 提供了一种新的标准化时间组件,由以下三部分组成:
- Clock:基于 Clock 协议实现,是一种计算时间的机制,定义了现在以及将来某个指定的时间点唤醒工作的方式。
- Instant:基于 InstantProtocol 协议实现,表示某个时间点。
- Duration:基于 DurationProtocol 协议实现,表示两个时间点之间的间隔。
Clock 协议定义如下,其中的关联类型 Instant
遵循 InstantProtocol
协议,而另一个关联类型 Duration
和 InstantProtocol
协议中的 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-0336、SE-0344。
本文仅是抛砖引玉,关于 Swift 5.7 更详细的变更请参考: Swift/CHANGELOG.md
文中涉及源码参考:Source code。