Swift 6 的时代来临了!苹果:它是超越 C++ 的最佳选择

2024-06-27 18:14:15 浏览数 (1)

整理|冬梅、核子可乐

策划 | Tina

在不久前刚结束的苹果全球开发者大会(WWDC)上,除了官宣万众瞩目的 Apple Intelligence 外,苹果还正式发布了 Swift 6.0。

Swift 上一次重大突破还是在 Swift 3 时代。但在全面发布之后,Swift 6 的种种变化可以说会将 Swift 3 轻松按在地上摩擦。这一方面是因为新引入的功能非常多;另一方面则是因为 Swift 最近的版本开始将大量功能隐藏在特性背后,并将在版本 6 中默认启用,所以很多修改可能根本就不为开发者们所察觉。

Apple 公司语言和运行时主管 Ted Kremenek 谈了 Swift 6 的发布,他认为 Swift 是替代 C 的最佳编程语言。“Swift 的安全性、速度和易用性,加上内置的 C 和 C 互操作性,意味着 Swift 是继任 C 的最佳选择,”Kremenek 说道。

他还坦言,这个版本已经开发了好几年。在会上,Kremenek 重点介绍了一个关键的新特性:编译时数据竞争安全——这是一个可选特性,因为它包含破坏性的更改。他还提到了嵌入式 Swift。这是 Swift 的一个子集,可用于微控制器和其他嵌入式系统。

关于 Swift 的新闻,最值得注意的一个方面是苹果为将这种语言推广到苹果平台之外所做的努力。Kremenek 表示,苹果正在“与开源社区合作,将 Swift 带到更多的平台和领域。”其目标包括在 Visual Studio Code(VS Code)和其他使用语言服务器协议(用于提供智能编辑特性)的编辑器中支持 Swift。除了现在已经支持的 Ubuntu、CentOS、Amazon Linux 和 Red Hat 之外,Linux 支持将扩展到 Debian 和 Fedora。Windows 支持也将得到改进。

Kremenek 介绍了一个新的 GitHub 组织,它将托管“对生态系统至关重要”的存储库。目前,其中包括用于维护变更建议的 swift-evolution,用于官方 VS Code 扩展的存储库,以及另一个名为 Swiftly 的项目。该项目用于从命令行管理 Swift 工具链,提供类似 Rust rustup 的体验。

在 Swift 官方网站的一篇文章中,Kremenek 指出,新组织还会包含其他的关键存储库,包括编译器、核心工具、标准库、示例和 Swift.org 网站内容。为尽量减少干扰,迁移将分阶段进行。

2015 年,在 Linux 的支持下,Swift 开源,人们希望它能成为一种广泛使用的语言,而不仅仅局限于苹果平台。然而,到目前为止,它在通用应用程序中的应用还很少。例如,IBM 是服务器端 Swift 的早期倡导者,他们创建了一个名为 Kitura 的开源框架来支持它,但大部分支持在 2019 年已经停止。

行业观察人士应该会注意到,Tim Sneath 于 2023 年 6 月加入苹果,担任 CoreOS 和开发工具 / 框架总监。Sneath 在推广跨平台应用框架方面有着丰富的经验——包括微软的 Silverlight。然后,最引人注目的是,他在谷歌管理过 Flutter 和 Dart 产品及用户体验。

另一个推动跨平台 Swift 的因素是 The Browser Company。该公司基于 Chromium 的 Arc 浏览器有些部分就是在 Windows 上使用 Swift 构建的。他们为此创建了 WinRT 绑定,现在都已经开源。

Swift 十年发展历程回顾

Swift 编程语言首次在 Apple 2014 年的全球开发者大会 (WWDC) 上亮相,自那时起就引发了相当大的争议。Apple 开发者工具部高级总监 Chris Lattner 早在 2010 年就开始设计这门新语言的基本概念。

Chris Lattner 于 2017 年 1 月在《Accidental Tech Podcast》一书中写道:“最初,我真的只是在瞎搞,没有人知道它,因为它没什么好了解的。但最终,它变得更加严肃了。于是我开始与我的管理层和一些正在开发 Clang 的工程师交谈,他们似乎对此很感兴趣。我们找了几个人兼职做这件事,我说服了我的经理人,我告诉他们这门语言很有趣,我们可以找几个人来做这件事。”

直到 2013 年,团队才解决了一个战略问题,即新语言如何融入现有的 Objective-C 生态系统。强迫所有 iOS 开发人员转向新语言可能会对社区产生重大破坏性影响。因此,该公司决定继续投资 Objective-C,同时致力于开发一种新的“安全编程语言” 。

一年后,注册的 Apple 开发人员能够接触到了这门新语言的测试版。据苹果 CEO Tim Cook 称,在其作为 Xcode 工具的一部分发布后的第一个月,它被下载了超过 1100 万次。

人们对 Swift 的最初反应褒贬不一。一些开发人员对其功能、灵活性和简单性感到高兴,而另一些人则批评它。然而,大多数人都认为 Swift 用于生产还为时过早。随着该语言进入了迅速发展期:每次新版本都会引入重大变化。

凭借快速、持续不断的重大改进,Swift 成为 2015 年“最受欢迎”的编程语言。

StackOverflow 关于最受欢迎技术的调查结果,来源:2015 年 StackOverflow 开发者调查

到了 2015 年,为了扩大 Swift 的开发者生态,苹果决定将 Swift 开源,此后,Swift 的发展势头更加迅猛。在宣布开源后的第一周,就有超过 6 万人在 Github 上克隆了 Swift 项目。

2016 年,Swift 3 和 Swift 包管理器发布。

2017 年,在以 Swift 3 的优势为前提的基础上,Swift 4 正式发布。该版本提供更高的稳健性和稳定性,提供与 Swift 3 的源代码兼容性,改进标准库,并添加存档和序列化等功能。该版本也被认为是当时最顺利的 Swift 重大更新,一次性更新所有代码用时甚至不到 1 小时。

2018 年,Swift 进行了泛型改进。随着对 conditional conformance 的支持,Swift 4.2 在泛型方面取得了重大进展,例如减少了样板代码的数量,使更多代码可重用等等。

2019 年,Swift 5.0 正式发布。它在 Apple 的平台上引入了应用程序二进制接口 (ABI) 的稳定版本。这是帮助开发人员在 iOS、macOS、tvOS、watchOS 和即将推出的 iPadOS 等专用操作系统上使用 Swift 的一大步。Apple 正在构建一个稳固的生态系统的野心已经昭然若揭,因为现在标准 Swift 库已包含在操作系统版本中。

在该版本中引入,它基本上是一个控件、图形元素和 Swift 应用程序布局库,可用于设计用户界面。此更新还带来了更新的文档、更新的 CLI Xcode 10.2 和 Ubuntu 二进制文件。Swift 也向后兼容其以前的版本。它现在可与之互操作的语言列表包括 Ruby、Python 和 JavaScript。

2020 年,Swift 5.3 发布。该版本带来了期待已久的官方平台支持扩展,包括 Windows 和其他 Linux 发行版从那时起,完整的 Swift 生态系统在 Windows 平台上可用。这包括编译器、标准库和核心库(dispatch、Foundation 和 XCTest)。

2021 年,Swift 5.5 将 Concurrency 加入了标准库,以增强 Swift 的并发编程能力。Swift 5.5 具有以下改进,例如使用 async/await 机制和参与者更好地支持并发。参与者是一种类似于类的引用类型,但与类不同的是,它们一次只允许一个任务访问它们的可变状态。还支持可发送数据,即可以安全地传输到另一个线程的数据。

2022 年,Swift 增加了 distributed actor 能力。

到了 2023 年,Swift 5.9 发布,并能支持 C 互操作特性。

值得注意的是,在过去五年间,开发团队没有对 Swift 进行过任何大版本更新——更具体地讲,在一半的生命周期当中,Swift 都处于 5.0 到 5.10 版本之间。

这种情况其实在很多编程语言项目中都发生过:目前最流行的几种编程语言都有着远超小众语言的版本发布周期:Python 3 耗时数年才与广大用户见面;PHP 6 拖的时间更长,以至于团队将其放弃并直接跳到了 PHP 7;Perl 6 同样拖了太久,最终演变成了名为 Raku 的另外一种新语言。

但 Swift 如此之久的停滞,很大一部分原因在于 Swift 核心创始成员的离开。

2022 年,Kremenek 也提到了前核心团队成员、LLVM 和 Swift 的设计者 Chris Lattner 的离职。2017 年 1 月,Chris 辞去了在苹果的工作,但还活跃在 Swift Evolution 社区中。

Chris 一直是 Swift 背后的中坚力量。2010 年 7 月开始,Chris 开始设计 Swift。完成基础架构后,Chris 带领开发小组陆续完成语法设计、编译器、运行时、框架、IDE 和文档等相关工作。

2022 年年初,在与社区成员的互动中,Chris 提到了他自 2021 年年中以来就不再是核心团队成员一事,在社区成员的追问下透露了自己离职的根本原因。

Chris 表示,“我决定离开核心团队的根本原因是糟糕的会议环境。催化剂是去年夏天的一次会议:在 WebEx 上被侮辱和大吼大叫之后 (这不是第一次,也不只是一个核心团队成员),我决定先休息一下。终于在去年秋天,我找领导层谈了谈,但他们找借口回避,并明确表示不会为此采取任何措施。因此,我决定不回去了。”

如今 Swift 6 已经发布,这次重大更新为 Swift 6 带来了哪些新变化?

Swift 6 有哪些新变化?

完全并发将默认启用

Swift 6 包含一大波围绕并发设计的更新,这里我们要为项目团队在新版本中实现的非凡进步而鼓掌喝彩。

到目前为止,最大的变化当数完全并发检查将默认启用。所以除非各位运气爆棚,否则您现有项目的代码大概率可能需要调整——Swift 团队在早期版本中将其设为可选项,以便留出时间给大家评估需要调整的部分。

Swift 6 则进一步改进了并发检查,Swift 团队表示这“消除了 5.10 版本中存在的大量误报性质的数据争用警告”。新版本还引入了多项具有针对性的变更,使得并发机制更易于采用——如果大家试用了 5.10 版本并觉得并发难以理解,也许新版本中的某些变更将会有所帮助。

其中最典型的当数 SE-0414,负责定义隔离区域以允许编译器最终证明代码中的不同部分是否可以同时运行。

这一变化的核心,体现在可发送性(sendability)的概念上。可发送类型是指能够在并发环境中安全传递的类型,具体可包括结构、具有常量属性的最终类、自动保护自身可变状态的 actor 等值类型。

在 Swift 6 之前,编译器一直非常严格:如果我们在某个 actor 上有一个不可发送的值,并尝试将其发送给另一 actor,则会收到并发检查警告。例如,尽管 SwiftUI 视图主体运行在主 actor 上,但 SwiftUI 视图本身却并非如此,这很容易导致编译器发出各种误报警告——就是说 Swift 会误认为存在潜在争用情形,但实际上并无问题。

我们可以通过以下代码理解这个问题:

代码语言:javascript复制
class User {
    var name = "Anonymous"
}

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
            .task {
                let user = User()
                await loadData(for: user)
            }
    }

    func loadData(for user: User) async {
        print("Loading data for (user.name)…")
    }
}

在 Swift 6 之前,对 loadData() 的调用会抛出一条警告:“在主 actor 隔离的上下文之外传递不可发送类型「User」参数,可能会引发数据争用。”但在 Swift 6 之后,这条警告将不再出现:Swift 现在能够正确检测到代码实际上并无问题,因为用户不会同时从两个或更多位置进行访问。编译器可以分析程序流程并意识到这种编写方式安全可行。

这一变化实际上上意味着,可发送对象现在要么符合 Sendable 条件,要么不需要符合 Sendable,具体安全性由编译器负责证明。这对开发者来说是一项显著的并发简化,其背后则是顶尖编译器开发水平的体现。

此外还有更多小幅改进,包括:

  • SE-430 会在需要在隔离区域之间发送值时,添加一个新的 sending 关键字。
  • SE-0423 改进了需要使用 Objective-C 框架时的并发支持。
  • SE-0420 允许我们创建 async 函数,其将被隔离至与 caller 相同的 actor。

其他一些变更则存在于 Swift 的早期版本当中,只是隐藏在特性标志背后。例如,SE-0401 删除了 Swift 5.5 中引入的一项功能:用于属性打包器的 actor 推断。

之前,任何使用带有 @MainActor 的属性打包器,作为其打包值的结构锃类都将自动被归入 @MainActor。也正因为如此,@StateObject 和 @ObservedObject 在使用它们的 SwiftUI 视图上才会传递其 main-actor 特性——只要大家在 SwiftUI 视图中使用其中任何一个属性打包器,整个视图都会变成 @MainActor。

例如考虑以下视图模型,使用 @MainActor 标记就是一种良好实践:

代码语言:javascript复制
@MainActor
class ViewModel: ObservableObject {
    func authenticate() {
        print("Authenticating…")
    }
}

但如果想要通过 @StateObject 从 SwiftUI 视图中使用,则必须在 Swift 6 及更高版本中借助 @MainActor 标记该视图,如下所示:

代码语言:javascript复制
@MainActor
struct LogInView: View {
    @StateObject private var model = ViewModel()

    var body: some View {
        Button("Hello, world", action: startAuthentication)
    }

    func startAuthentication() {
        model.authenticate()
    }
}

在 Swift 6 之前,@MainActor 会因其 @SStateObject 属性而被赋予整个视图。

Swift 6 中启用的另一项原有变更为 SE-0412,要求全局变量在并发环境中必须是安全的。

这一条用于约束项目当中可能作用于全局范围的松散变量:

代码语言:javascript复制
var gigawatts = 1.21

同时也适用于存储在类型中的静态变量:

代码语言:javascript复制
struct House {
    static var motto = "Winter is coming"
}

这些数据可以在任意时间、任意地点进行访问,因此在本质上不够安全。要解决这个问题,我们需要将变量转换为可发送常量,将其限制为全局 actor(例如 @MainActor),或者在没有其他选择或明确知晓其在其他位置受到保护时,将其标记为非隔离。

例如,允许采取以下几种方法:

代码语言:javascript复制
struct XWing {
    @MainActor
    static var sFoilsAttackPosition = true
}

struct WarpDrive {
    static let maximumSpeed = 9.975
}

@MainActor
var idNumber = 24601

// 除非确定安全,否则不建议采取这种方式

代码语言:javascript复制
nonisolated(unsafe) var britishCandy = ["Kit Kat", "Mars Bar", "Skittles", "Starburst", "Twix"]

之前就已存在、但现在默认启用的另一项功能为 SE-0411,它会将函数默认值变更为与其所在函数具有相同的隔离度。

例如,以下代码形式在旧版本中会触发错误,但新版本允许运行:

代码语言:javascript复制
@MainActor
class Logger {

}

@MainActor 
class DataController {
    init(logger: Logger = Logger()) {

    }
}

由于 DataController 和 Logger 都已被限制为 main actor,因此 Swift 认为 Logger() 的创建也应被限制为 main actor,这也确实合理。另外请记住:如果 Swift 6 就大家的代码抛出了并发方面的警告和错误,其实这些问题之前也存在,只是旧版本没法自动诊断出来。

count(where:)

SE-0220 引入了一种新的 count (where:) 方法,它会执行相当于 filter() 和 count 的单次传递。这能节约下创建即抛新数组的时间,从而为常见问题提供清晰简洁的解法。

以下示例创建了一个测试结果数组,可以计出结果大于或等于 85 的数量:

代码语言:javascript复制
let scores = [100, 80, 85]
let passCount = scores.count { $0 >= 85 }

下面则可以计出数组当中以“Terry”开头的名称数量:

代码语言:javascript复制
let pythons = ["Eric Idle", "Graham Chapman", "John Cleese", "Michael Palin", "Terry Gilliam", "Terry Jones"]
let terryCount = pythons.count { $0.hasPrefix("Terry") }

这种方法适用于所有符合 Sequence 的类型,意味着我们也可以在 sets 和 dictionaries 中使用。

注意:count(where:) 最初计划于 2019 年登陆 Swift 5.0,但当时出于性能考虑而被撤回。

类型化抛出

SE-0413 可以指定函数能够抛出哪些具体的错误类型,即所谓“类型化抛出”。这解决了 Swift 中的一个老问题:即使我们已经明确捕捉了所有可能的错误,仍需要使用通用的 catch 子句。

作为类型化抛出的示例,我们可以定义一个 CopierError 来跟踪复印机何时用尽纸张:

代码语言:javascript复制
enum CopierError: Error {
    case outOfPaper
}

之后我们可以创建一个 Photocopier 结构,由它创建一定数量的页面副本。如果现有纸张不足以完成请求的操作,则可能提示抛出错误。但与其简单标记为 throws,我们不妨使用 throws(CopierError) 来明确到底抛出了哪种错误:

代码语言:javascript复制
struct Photocopier {
    var pagesRemaining: Int

    mutating func copy(count: Int) throws(CopierError) {
        guard count <= pagesRemaining else {
            throw CopierError.outOfPaper
        }

        pagesRemaining -= count
    }
}

注意:通过此项变更,大家可以使用 throws 来指定要抛出的错误类型,或者使用 throws(OneSpecificErrorType) 来强调只能抛出特定类型。大家无法编写 throws(A, B, C) 来定义多种可抛出的错误。现在我们可以编写以下代码来尝试复印,并捕捉可能抛出的单个错误:

代码语言:javascript复制
do {
    var copier = Photocopier(pagesRemaining: 100)
    try copier.copy(count: 101)
} catch CopierError.outOfPaper {
    print("Please refill the paper")
}

这里的重要变化体现在调用方面:在早期 Swift 版本中,我们需要在最后进行所谓的“宝可梦捕捉”,因为 Swift 自身无法确定可能抛出哪些错误类型,所以必须由开发者“全部一一捕捉”。此项变更还有其他几个优势:

  1. 由于 Swift 知晓 CopierError 是唯一可以招聘的错误类型,所以我们可以编写 throw.outOfPaper。
  2. 如果 do 部分的代码仅抛出一种错误,则常规 catch 部分的 error 值将自动具有相同的错误类型,而非任意类型。
  3. 如果我们尝试抛出 throws 子句中未列出的任何其他错误类型,则 Swift 会发出编译错误。

更巧妙的地方在于,throws(any Error) 会等价于直接使用 throws 本身,而 throws(Never) 等价于非抛出函数。这听起来似乎非常晦涩,但简单理解就是现在我们可以在很多地方更明确地表达 rethrows:该函数会抛出任何由该函数参数抛出的内容。

举例来说,Swift 6 中的新 count(where:) 方法接受一个闭包,用于评估有多少个项与当前运行的任何类型的过滤器相匹配。该闭包可能会抛出错误,在此情况下 count(where:) 也会抛出相同的错误类型:

代码语言:javascript复制
public func count<E>(
    where predicate: (Element) throws(E) -> Bool
) throws(E) -> Int {

如果该闭包不抛出错误,则 throws(E) 实际上就是 throws(Never),也就是说 count(where:) 同样不会抛出错误。

尽管类型化抛出看似极具吸引力,但当可以抛出的错误未来发生变化时,使用这种新机制也许并不是什么好选择。特别需要注意的,就是尽量不要将其添加到库代码中,这相当于把我们锁定在一份自己未来可能不想遵守的合同身上。

事实上,演进提案作者们给的意见倒是非常中肯,这里分享给大家:尽管 Swift 中引入了类型化抛出,但在大多数情况下,仍然是非类型化抛出更加适用。

类型化抛出最适合的场景,就是在愈发重要的嵌入式 Swift 开发领域,其中特别重视性能与可预测性。苹果最近对类型化抛出的关注表明,嵌入式 Swift 已经成为他们的投资优先目标——所以距离内核级 Swift 的出炉也已经为期不远了。

包迭代

SE-0408 引入了包迭代,能够对 Swift 5.9 带来的参数包机制执行循环遍历。

尽管值包仍然是 Swift 当中最复杂的特性之一,但演进提案仅用几行代码就实现了对任意元数的元组比较机制,轻松展现了该特性的实际意义:

代码语言:javascript复制
func == <each Element: Equatable>
(lhs: (repeat each 
Element), rhs: (repeat each Element)) -
> Bool {
    for (left, right) in repeat (each lhs, 
 each rhs) {
 
        guard left == right else { return false }
    }
    return true
}

有些朋友可能不太理解,简单来讲就是 SE-0015 增加取款最多 6 个元组进行直接比较的支持,也就是说可以使用 == 比较最多包含 6 个条目的两个无级。如果试图比较的无级中包含 7 个条目——例如 (1, 2, 3, 4, 5, 6, 7) == (1, 2, 3, 4, 5, 6, 7),则 Swift 会抛出错误。SE-0408 的出现,消除了以上代码中的这一限制。

更让人激动的是,演进提案中的未来方向部分提到,后续我们可能还将迎来 Swift 的 zip() 函数变体,其将支持任意数量的序列。

话虽如此,但据我们推测,这项特定功能更像是苹果在努力将 SwiftUI 已经使用了一段时间的技术给标准化:即能够在 VStack 中迭代 TupleView 的子项。

在非连续元素上添加集合操作

SE-0270 引入了多种新方法以处理集合上的复杂操作,例如移除或者删除不连续的多个条目。

此项变更由名为 RangeSet 的新类型提供支持。如果大家曾经使用过 Foundation 中的 IndexSet,则可以把 RangeSet 理解为 IndexSet,唯一的区别就是其代表任意 Comparable 可比较类型、而非单纯整数。

许多 Swift API 都已升级为 RangeSet。作为示例,我们可以创建一个包含学生考试成绩的数组,如下所示:

代码语言:javascript复制
struct ExamResult {
    var student: String
    var score: Int
}

let results = [
    ExamResult(student: "Eric Effiong", score: 95),
    ExamResult(student: "Maeve Wiley", score: 70),
    ExamResult(student: "Otis Milburn", score: 100)
]

我们可以从中提取一个 RangeSet,其中包含所有得分达到 85% 或者更高的学生的索引,具体如下:

代码语言:javascript复制
let topResults = results.indices { student in
    student.score >= 85
}

如果想要访问这些学生,则可以使用新的 Collection 下标:

代码语言:javascript复制
for result in results[topResults] {
    print("(result.student) scored (result.score)%")
}

此下标会返回另一个名为 DiscontiguousSlice 的新类型。它与 Slice 相似,但出于性能考虑,它的用法是引用存储在不同集合中的元素,只是其索引是不连续的,也就是说各元素在集合当中不一定相邻。

之所以名称当中要使用“set”,是因为 RangeSet 支持来自 SetAlgebra 协议的各种函数,包括 union()、intersection() 和 isSuperset(of:)。也就是说可以将一个范围插入另一个范围并合并所有重合范围,而非创建重复项。

导入声明上的访问级修饰符

SE-0409 增加了使用访问控制修饰符以标记导入声明的功,例如 private import SomeLibrary。

这项新机制有多种用途,包括帮助库开发人员避免意外泄露自己的依赖项。例如,银行业务可能会被拆分成多个部分:

  • 应用本体,负责呈现用户界面。
  • 处理所有功能和核心逻辑的银行业务库。
  • 几个较小的内部库,负责处理较低级别的特定工作,例如交易包、网络包等。

可以看到应用本体依赖于银行业务库,而银行业务库又依赖于交易、网络及其他内部库。

我们可以使用代码来演示这样一套设置,代码中还将涉及需要解决的问题。首先,我们假设低级交易包具有以下结构:

代码语言:javascript复制
public struct BankTransaction {
    // 省略具体代码
}

在银行业务库中,我们可能会编写一条函数,使用该 BankTransaction 函数将资金从一个账户发往另一账户:

代码语言:javascript复制
public func sendMoney(from: Int, 
to: Int) -> BankTransaction {   
    // 处理资金发送操作,而后返回结果   
    return BankTransaction()
}

现在,我们可以在主应用上调用 sendMoney() 来完成这项工作。

这些都是很常规的 Swift 代码,但其中令人头痛的问题在于:打包器库通常并不想透露自己内部依赖库的工作机制,可主应用还会被授予从交易库访问 BankTransaction 结构的权限。实际上,它应该只使用银行业务库中的 API。

从 6.0 版本开始,可以通过在交易导入上使用访问控制来解决这个问题:通过在银行业务库中使用 internal import Transactions 或者类似的方法,Swift 即可拒绝构建任何声明为公共、公开的来自交易库的 API 代码。

这显然有助于更好地明确代码边界:银行业务框架仍然能继续在内部使用其需要的所有库,但又不允许意外将这些库发送回客户端(在本示例中为应用端)。如果我们确认需要公开内部框架类型,则可使用 public import Transactions 来实现。

在更细粒度的级别上讲,这还允许为同一模块内的文件添加额外的限制——一个文件可以私下导入一套框架,而不致意外在其他位置公开该框架的内容。

虽然 Swift 6 尚未正式发布,但从目前的演示来看,在 Swift 6 模式下运行时,导入的默认设置也将为 internal;同时在 Swift 5 模式下则为 public,以保持与现有代码的兼容性。

不可复制类型迎来升级

不可复制(noncopyable)类型是在 Swift 5.9 版本中引入的,但在 Swift 6 中迎来了一系列升级。

这里要提醒大家,不可复制类型允许我们创建具有唯一所有权的类型,并根据需要通过借用或使用来传递这些类型。

我们这里以《碟中谍》电影中的机密信息传递方式为例——这类信息会在读取后被自动销毁。因此,我们也可以使用读取后即销毁的不可复制类型建立以下模型:

代码语言:javascript复制
struct Message: ~Copyable {
    var agent: String
    private var message: String

    init(agent: String, message: String) {
        self.agent = agent
        self.message = message
    }

    consuming func read() {
        print("(agent): (message)")
    }
}

func createMessage() {
    let message = Message(agent: "Ethan Hunt", message: "You need to abseil down a skyscraper for some reason.")
    message.read()
}

createMessage()

在这部分代码中,编译器会强制要求 message.read() 只能被调用一次,因为它会消耗掉指定对象。

这项机制会影响到提案引入的其他变更。例如,由于 Swift 的 Optional 实现了泛型枚举,因此不可复制类型现在可以与泛型一同使用,从而提供可选的不可复制实例等应用。但由于泛型类参数会自动符合 Copyable,所以我们必须使用~Copyable 明确选择退出。

同样的,此项变更意味着不可复制类型现在也可以符合协议,但前提是将这些协议也标记为~Copyable。否则,这些类型就会自动加入 Copyable,如前文所述。(顺带一提,Copyable 类型也可以良好符合不可复制协议。)

SE-0429 通过添加对不可复制值的部分使用,进一步完善了这种情况。

以往,将一种不可复制类型合并至另一种不可复制类型时可能引发问题。例如在 SE-4029 之前,哪怕如此简单的代码都有可能存在问题:

代码语言:javascript复制
struct Package: ~Copyable {
    var from: String = "IMF"
    var message: Message

    consuming func read() {
        message.read()
    }
}

但现在只要相关类型中不包含反初始化器,上述代码在 Swift 中即被视为有效。

关于不可复制的第三大改进为 SE-0432,允许我们在切换不可复制类型时对其执行借用。以往,我们无法使用依赖于不可复制值的 where 子句进行模式匹配,但在 SE-0432 的帮助下,现在 Swift 6 已经可以实现这一点。

仍然以《碟中谍》场景为例,我们假设一组 orders 为签名或者匿名,如下所示:

代码语言:javascript复制
enum ImpossibleOrder: ~Copyable {
    case signed(Package)
    case anonymous(Message)
}

由于该枚举具有不可复制的关联值,因此其本身也必须是不可复制的。但是关联值不可复制,意味着很难使用 where 对其进行模式匹配。具体来讲,当我们想要对一种 Message 类型执行一组操作,而对另一种 Message 类型执行另一组操作,就会发现实现起来相当费劲。

有了 SE-0432,这个问题已经得到解决,我们可以直接使用以下代码:

代码语言:javascript复制
func issueOrders() {
    let message = Message(agent: "Ethan Hunt", message: "You need to abseil down a skyscraper for some reason.")
    let order = ImpossibleOrder.anonymous(message)

    switch consume order {
    case .signed(let package):
        package.read()
    case .anonymous(let message) where message.agent == "Ethan Hunt":
        print("Play dramatic music")
        message.read()
    case .anonymous(let message):
        message.read()
    }
}

总之,这一系列变更有助于在 Swift 中更加顺畅丝滑地运用不可复制类型。

128 位整数类型

SE-0425 中引入了 Int128 与 UInt128。这方面本文不做过多赘述,相信大家都知道它们的具体作用。就连演进提案也承认,“具体类型 API 无甚可聊。”

开发者怎么看?

时间快进到 2024 年,开发者对于 Apple 支持的 Swift 还是像以前一样分为乐观派和悲观派。看好 Swift 前景的开发者认为,Swift正在快速发展。在 Apple 开源之后,许多开发人员开始为 Swift 做出贡献。如今,Swift 已不局限于 Apple 生态系统。它也已开始在 Linux 环境中无缝运行。甚至他们也开始编写服务器端 Swift 框架。Swift 速度快、效率高、内存占用少。

而不看好 Swift 的开发者则认为:“尽管 Swift 是开源的,并且深受开发人员的喜爱,但它很可能仍仅限于 iOS/Mac 应用程序开发。由于 React native/Flutter 等跨平台技术的发展,这些技术可以生成原生应用程序,这将削减 Swift 开发人员的就业市场。”

参考链接:

https://www.altexsoft.com/blog/the-good-and-the-bad-of-swift-programming-language/

https://devclass.com/2024/06/13/after-ten-years-of-swift-apple-promises-ai-powered-tooling-and-another-push-toward-cross-platform/

https://medium.com/@niranjan9795/swift-from-new-baby-to-10-year-old-child-b3cc2ba98450

https://www.hackingwithswift.com/articles/269/whats-new-in-swift-6

0 人点赞