大多数浏览器和
Developer App 均支持流媒体播放。
-
在 Swift 中使用不可拷贝的类型
开始探索 Swift 中不可拷贝的类型。了解拷贝在 Swift 中的含义、何时需要使用不可拷贝的类型,以及值所有权如何让你清晰声明自己的意图。
章节
- 0:00 - Introduction
- 0:30 - Agenda
- 0:50 - Copying
- 7:05 - Noncopyable Types
- 11:50 - Generics
- 19:12 - Extensions
- 21:24 - Wrap up
资源
- Copyable
- Forum: Programming Languages
- Swift Evolution: Borrowing and consuming pattern matching for noncopyable types
- Swift Evolution: Noncopyable Generics
- Swift Evolution: Noncopyable Standard Library Primitives
相关视频
WWDC19
-
下载
嗨 我是来自 Swift 团队的 Kavon 欢迎来到“在 Swift 中 使用不可拷贝的类型”! 你和我都是独一无二的 但 Swift 中的值却不是 这是因为值可以被拷贝 在编程时 保证值的唯一性 是一个深刻的概念 很高兴地告诉大家 我们最近在 Swift 中 引入了不可拷贝类型! 今天 我们要介绍许多精彩主题 首先来了解一下拷贝的工作原理 然后 我们将介绍 所有权和不可拷贝类型 最后我将介绍更高级的主题 例如以泛型方式使用不可拷贝类型 和编写这些类型的扩展 那么 让我们开始进行拷贝! 我正在开发一款新游戏 因此我定义了一个 Player 类型 他们的图标以表情符号表示 让我们来创建两个玩家 到目前为止 他们是一样的 凭直觉我觉得 更改一个玩家的图标 不会影响另一个 让我们逐步分析一下 这种直觉背后的逻辑 从第一行开始 player1 是一只青蛙 这个变量的内容是 构成 Player 的实际数据 这是因为它是一种结构 即一种值类型 接下来是 player2 我将它设为与 player1 相同 但这到底意味着什么? 这意味着我正在拷贝 player1 当拷贝一个变量时 实际上是在拷贝它的内容 因此 当我更改 player2 的图标时 实际上是在更改 独立于 player1 的玩家 我甚至不用考虑拷贝或销毁值 Swift 会处理这些
但如果 Player 是引用类型呢? 这意味着我将它从结构改成了类 如果代码与之前一样 会怎样? 让我们分解剖析第一条语句
在构建 PlayerClass 时 会单独分配一个对象 来存储它的数据
player1 的内容 将成为这个对象的自动管理引用 这就是引用类型的含义 在下一条语句中 player2 与 player1 相同 这意味着拷贝了引用 而不是对象本身! 这有时被称为浅拷贝 执行速度非常快 由于两个玩家引用同一个对象 因此更改图标 会同时更改两个玩家的图标 因此断言并不成立 请注意 在这两种情况下 拷贝的工作原理是一样的 唯一的区别在于 拷贝的是值还是引用 你可以通过定义 构造器来进行深拷贝 从而使引用类型的行为 与值类型一样 以这个构造器为例 它以递归方式重新创建对象 以及对象指向的所有内容
这是通过调用 Icon 的构造器 以使用另一个玩家的图标 重新创建对象来实现的 这有助于确保新的玩家 不会与另一个玩家共享引用
让我们回到之前的程序状态 在将 player2 设为 与 player1 相同后 看看深拷贝如何改变行为
现在 两个玩家 引用的是同一个对象
在编写玩家的域之前 我现在将通过调用 它自身的构造器来进行深拷贝
这会分配一个完全相同的独立对象 从而确保其他变量 不会受到变动影响
事实上 这就是写时拷贝的本质 它可以实现在发生变动时 保持独立性 因此你可以获得 与值类型相同的行为
在设计新的类型时 你已经能控制是否有人 可以深拷贝它的值 你无法控制 Swift 能否自动拷贝它 Copyable 是一种新协议 描述自动拷贝类型的功能 与 Sendable 一样 它没有成员要求
在 Swift 中 默认情况下 所有类型均被视为 Copyable 类型 这里说的是所有类型 每个类型都会尝试 自动符合 Copyable 每个泛型参数都会 自动要求输入的类型 为 Copyable 类型 每个协议和关联类型 都会自动要求具体类型 符合 Copyable
每个装箱的协议类型 都自动带有 Copyable
你不必像我一样 自行编写 Copyable 它已经存在 即使没有显示出来 Swift 认为你需要拷贝功能 因为处理 Copyable 类型 更加简单
但是 在某些情况下 拷贝会使代码容易出错 假设我正在处理一个类型 用于为后端的银行转账进行建模 在现实生活中 转账有三种状态: 待处理、已取消、已完成
我们将为这一类型 提供 run 方法以完成转账 我需要安排转账 这里显示了用于安排转账的函数 显而易见 如果我不小心 运行了两次转账 用户会感到不满 因此让我们仔细检查一下 如果延迟小于一秒 我希望立即运行 但我忘了编写 return 语句 因此我会失败 这个函数会再次运行 像这样的简单错误会造成巨大损失 那么我该如何避免呢? 我可以添加一个变量 来跟踪转账状态 这样断言就能捕捉到再次运行尝试 但是除非我编写的测试能够命中错误 否则断言不会发现错误 因此 我可能仍然会遇到这样的错误 从而导致后端服务瘫痪 事实上 这个 schedule 函数 还存在另一个错误! 试想一下 如果处于休眠状态的 任务被取消 会发生什么 如果调用方不注意检查 引发的错误 他们可能会忘记取消转账
我可以在 BankTransfer 中 引入 deinit 以免忘记取消 但这实际上相当无用
仔细看一下 startPayment 函数 它保留 transfer 的副本 以便跟踪是否已安排转账 这是个问题 因为除非销毁 transfer 的所有副本 否则 transfer 的 deinit 不会运行 这就是问题的根源: 我无法控制在我的程序中 这个 transfer 有多少个副本正待处理 因此 虽然拷贝值功能通常 是适合类型的默认设置 但在某些情况下 最好使用不可拷贝类型 暂且将 BankTransfer 的问题 放在一边 我们来了解一下不可拷贝类型
假设我想为 FloppyDisk 建模 如果这样编写 类型就会 默认符合 Copyable 但是 如果在声明符合性的位置 我在 Copyable 一词前面 加上波浪号 则表示我禁用了 默认符合 Copyable 的设置 现在 FloppyDisk 完全不符合 Copyable! 可以将波浪号 Copyable 视为 声明你标记的类型不可拷贝 当你试图拷贝软盘时 会发生什么? 不支持拷贝 因此 Swift 会消费它
我可以通过编写 consume 来显式实现 但无论如何都会发生
消费变量会获取变量的值 但不会初始化变量
因此 在消费之前 只有系统磁盘会被初始化
消费会将系统磁盘的内容移出 并移入备份磁盘 之后读取系统磁盘的内容会出错 因为其中没有任何内容
现在思考一下 这个创建新磁盘的函数 当调用 format 时 变量 result 会发生什么? 这很难说 因为函数的签名并没有声明它需要 对磁盘拥有何种所有权 对于可拷贝参数 你不必考虑这个问题: format 会有效地收到磁盘副本 但是 对于不可拷贝参数 你必须声明函数 对值拥有何种所有权 因为我们无法拷贝
第一种所有权称为 consuming 这意味着函数将从调用方获取参数 它属于你 因此你甚至可以进行更改
但是 在这里使用 consuming 会有问题 因为 format 不会返回磁盘 它不会返回任何内容 不过 想想就知道 格式化磁盘只需要 对磁盘具有临时访问权限
当对某个物品具有临时访问权限时 你是在借用它 borrowing 会授予你对参数的 读取访问权限 就像 let 绑定一样
在底层 这是适用于 Copyable 类型的 几乎所有参数和方法的运作方式
不同的是 你不能消费或更改 显式借用的参数 你只能拷贝 由于 format 函数 最终需要更改磁盘 因此也不能使用 borrowing
最后一种所有权你已经很熟悉了 那就是 inout! 对于方法来说 它等同于写入更改
Inout 提供对调用方变量的 临时写入访问权限 由于你具有写入访问权限 因此可以消费参数 但是你必须在函数结束前的 某个时刻 重新初始化 inout 参数 因为当返回时 调用方希望获得一个返回值 好了 让我们回过头来 看看 BankTransfer 示例 因为现在我们可以将它作为 可消费资源进行建模 首先我将 BankTransfer 设为不可拷贝结构 并将 run 方法标记为 consuming 这样就可以从调用方那里 获取自身的值 仅凭这两处更改 我便不再需要断言了 Swift 可以保证我无法 对同一转账交易调用 run 方法两次 事实上 它的所有权被精确跟踪 因此我可以为这个结构添加 deinit 以便实现在被销毁而不是“run”时 触发一个动作 由于“run”方法在结束时 还将自动自行销毁 因此我将在这里编写 discard self 这将销毁它 但不会调用 deinit 我们来看看 schedule 函数中的 那些错误会发生什么 首先我必须为 transfer 参数 添加所有权 由于“schedule”应该最后使用 因此合理的做法是消费它 现在 当我尝试编译这个函数时 会发现 Swift 已消除了所有错误 由于 if 语句会失败 因此可能会消费 transfer 两次 添加 return 可以防止这种情况 那么另一个错误呢? 它也已被消除 schedule 是 transfer 的 最后一个所有者 因此如果引发 sleep deinit 将运行 从而取消转账
不可拷贝类型是 提高程序正确性的出色工具 你可能想在各处使用它们 包括泛型代码 在 Swift 6 中 现在你可以使用不可拷贝泛型! 这并不是什么新的泛型系统 它建立在 Swift 的 现有泛型模型之上 让我们重新认识一下泛型
首先思考一下 Swift 中的类型体系 所有类型在这里都能友好共存 无论是 String 类型 还是我自己的 Command 类型
我的协议 Runnable 在这个体系中定义了一个子空间 其中包含符合这项协议的类型 现在 没有符合这项协议的类型 因此子空间是空的 但是 如果我使用 Runnable 符合性扩展 Command 那么相应点就会移至 Runnable 空间中 泛型背后的核心理念是 使用符合性约束来描述泛型类型 让我们通过这个名为 execute 的 泛型函数来思考这个问题 注意大括号中的 T 它声明了一个新的泛型类型参数 表示这个体系中的某个类型 但我们不知道是哪个类型 还记得吗? 我说过 Copyable 广泛存在 这个 T 应用的是默认约束 这就要求输入类型符合 Copyable
Command 默认符合 Copyable Runnable 也继承自 Copyable 事实上 之前 Swift 中整个体系的类型 一直都是隐式 Copyable 类型
这意味着这个空间中的所有类型 都可以传入 execute 函数 因为唯一的约束就是 T 符合 Copyable
因此 尽管 Command 也符合 Runnable 但它仍然存在于 Copyable 这个范围更广的空间中 在这个空间内 特定类型可能是 Runnable 类型 也可能不是 泛型参数 T 并不排除 具备其他符合性的类型 如编写的代码所示 execute 函数有望正常运行 而不需要具备除 Copyable 之外的任何其他符合性
不过 为了实现 execute 函数 我确实希望 T 可运行 因为我需要调用 run 方法 因此 我将使用 where 子句 对 T 添加 Runnable 约束
通过这样做 我进一步约束了 T 所允许的类型空间 现在是 Runnable 和 Copyable 这个范围较小的空间 这包括 Command 但现在排除了 String 因为 String 不符合 Runnable
自 Swift 5.9 以来 体系有所扩展 因为一些类型不符合 Copyable 例如 实用的新类型 BankTransfer 就不符合 因此相应点位于空间之外 由于只能通过编写波浪号 Copyable 禁用 Copyable 符合性 我将这个范围更大的空间 称为波浪号 Copyable
Swift 中你所熟悉的 大多数类型都符合 Copyable 那么 它们怎么会被包含在 波浪号 Copyable 中?
如前所述 在这个范围更大的空间里 不能假定任何特定类型 符合 Copyable 它可能可拷贝 也可能不可拷贝 你应该这样理解波浪号 Copyable
那么 Any 类型呢? 它始终可拷贝 也应该如此 几乎所有编程语言中的 Any 类型都可拷贝
现在我们已经准备就绪 可以探讨不可拷贝泛型了 让我们首先探讨之前提到的 Runnable 协议
目前 类型体系是这样的 所有可运行值都可拷贝
BankTransfer 不可拷贝 因此也不可运行 但我希望 BankTransfer 具备符合性 以便我能够以泛型方式使用它 拷贝 Runnable 类型的功能 并不是协议的基本要求 因此我将通过添加波浪号 Copyable 解除 Runnable 中的 Copyable 约束
这样就改变了层次结构 Copyable 空间只与 Runnable 重叠 而不是包含 Runnable Command 既可运行又可拷贝 因此它位于重叠的空间内 接下来 如果使用 Runnable 符合性 来扩展 BankTransfer 相应点就会移至 Runnable 内 而不在 Copyable 内
让我们回过头来看看 泛型函数 execute
泛型参数 T 仍应用默认约束 因此 只有像 Command 这样 既可运行又可拷贝的类型 才被许可在 execute 中使用
让我们使用波浪号 Copyable 解除 T 的 Copyable 约束
解除这一约束后 许可类型 将扩展为所有 Runnable 类型 execute 函数将声明 T 不可拷贝 关键在于: 常规约束通过指定更加具体的条件 来缩小许可类型的范围 而波浪号约束通过设置更加 宽泛的条件来扩大范围 好了现在 让我们 将所有理论付诸实践 我定义了一些 Runnable 类型 我希望将它们封装在 一个名为 Job 的新结构中 我为 Job 定义了一个 泛型参数 Action 它可运行 但不可拷贝 但根据到目前为止 我编写的内容 我会遇到错误 结构 Job 默认符合 Copyable 因此它只能包含 Copyable 数据 你可以通过两种方式将不可拷贝值 存储在另一个结构中 或者置于类中 因为拷贝类仅拷贝引用; 或者禁用包含类型 本身的 Copyable 我选择第二种方法 将 Job 设为不可拷贝
我仍可以为 Action 输入 Command 类型 因为 Action 不会 禁止显示 Copyable 类型 Job 声明它不需要拷贝 Action 因此也可以使用不可拷贝类型 但如果我知道为 Action 输入的 类型为 Copyable 类型 会怎样? 那么 Job 可拷贝 因为它是 Action 的容器
作为 API 编写人员 我可以通过声明 Job 可有条件地拷贝来实现这一点 这个扩展声明 当 Action 可拷贝时 Job 也可拷贝
这在我们的体系中是什么样子的?
在我们为 Action 输入具体类型之前 我们不知道 Job 是否可拷贝 让我们输入 Command
我们知道 Command 可拷贝 因此 Command-Job 也可拷贝
但如果我将 Action 设为 BankTransfer 由于不满足条件符合性 因此 BankTransfer-Job 不可拷贝
因此 不可拷贝泛型 背后的整体理念是 解除默认的 Copyable 约束 你已经了解了如何定义 具有不可拷贝泛型参数的类型 让我们进一步了解这一类型的扩展 假设我想为 Action 定义一个 getter 方法
我将使用 Job 的普通扩展 来添加这一方法
直接调用即可... 但这不会拷贝 Action 吗? 会返回 action 确实会拷贝它 在扩展中这并不属于错误
因为这个普通扩展的 默认约束条件是 Job 其中 Action 可拷贝
因此 这个 getter 是正确的 因为无法对诸如 BankTransfer 之类的作业调用这一方法 这就是所有扩展的运作方式: 扩展类型作用域中的所有泛型参数 都具有约束条件 Copyable 这包括协议中的 Self
扩展以这种方式运作 具有一项重要好处 假设 Job 实际上是并非由我编写的 某个 JobKit 模块的一部分 这里所示的协议 描述 Cancellable 类型 假设我不知道什么是不可拷贝类型 但我还是希望使 Job 符合这项协议 没关系 因为我可以编写这个扩展 这样就可以了
这是因为符合性默认 以 Action 可拷贝作为条件 因为一般来说 Action 不可拷贝 当 Action 可拷贝时 Job 也可拷贝 这意味着它符合 Cancellable
因此 你可以发布这个 Job 类型 这样 仅使用 Copyable 类型的 程序员也可以使用它 现在 如果我希望 使这个扩展适用于所有作业 无论是否可拷贝 该怎么做?
那么 我只需在这个扩展中解除 Action 的 Copyable 约束即可 现在 Job 符合 Cancellable 而无需假定 Action 可拷贝
今天 我们了解了在 Swift 中 拷贝的工作原理及它可能带来的挑战 不可拷贝类型是一种实用的工具 可提高程序的正确性 但需要权衡考虑所有权 我们已经朝着在标准库中 采用不可拷贝泛型迈出了第一步 为此加入了 Optional、UnsafePointer、Result 你可以通过阅读 Swift Evolution 提案 进一步了解以下主题: 不可拷贝泛型、 借用和消费模式匹配 以及不可拷贝标准库基元 你还可以在《Swift 编程语言》 一书中了解更多信息 此外 如果你想了解“写时拷贝” 和设计泛型类型的最佳做法 请观看 WWDC 2019 中的 “现代 Swift API 设计” 谢谢大家 祝你们在 WWDC 度过美妙的时光
-
-
0:52 - Player as a struct
struct Player { var icon: String } func test() { let player1 = Player(icon: "🐸") var player2 = player1 player2.icon = "🚚" assert(player1.icon == "🐸") }
-
1:55 - Player as a class
class PlayerClass { var icon: String init(_ icon: String) { self.icon = icon } } func test() { let player1 = PlayerClass("🐸") let player2 = player1 player2.icon = "🚚" assert(player1.icon == "🐸") }
-
3:00 - Deeply copying a PlayerClass
class PlayerClass { var data: Icon init(_ icon: String) { self.data = Icon(icon) } init(from other: PlayerClass) { self.data = Icon(from: other.data) } } func test() { let player1 = PlayerClass("🐸") var player2 = player1 player2 = PlayerClass(from: player2) player2.data.icon = "🚚" assert(player1.data.icon == "🐸") } struct Icon { var icon: String init(_ icon: String) { self.icon = icon } init(from other: Icon) { self.icon = other.icon } }
-
5:10 - Copyable BankTransfer
class BankTransfer { var complete = false func run() { assert(!complete) // .. do it .. complete = true } deinit { if !complete { cancel() } } func cancel() { /* ... */ } } func schedule(_ transfer: BankTransfer, _ delay: Duration) async throws { if delay < .seconds(1) { transfer.run() } try await Task.sleep(for: delay) transfer.run() } func startPayment() async { let payment = BankTransfer() log.append(payment) try? await schedule(payment, .seconds(3)) } let log = Log() final class Log: Sendable { func append(_ transfer: BankTransfer) { /* ... */ } }
-
7:46 - Copying FloppyDisk
struct FloppyDisk: ~Copyable {} func copyFloppy() { let system = FloppyDisk() let backup = consume system load(system) // ... } func load(_ disk: borrowing FloppyDisk) {}
-
8:18 - Missing ownership for FloppyDisk
struct FloppyDisk: ~Copyable { } func newDisk() -> FloppyDisk { let result = FloppyDisk() format(result) return result } func format(_ disk: FloppyDisk) { // ... }
-
9:00 - Consuming ownership
struct FloppyDisk: ~Copyable { } func newDisk() -> FloppyDisk { let result = FloppyDisk() format(result) return result } func format(_ disk: consuming FloppyDisk) { // ... }
-
9:26 - Borrowing ownership
struct FloppyDisk: ~Copyable { } func newDisk() -> FloppyDisk { let result = FloppyDisk() format(result) return result } func format(_ disk: borrowing FloppyDisk) { var tempDisk = disk // ... }
-
9:55 - Inout ownership
struct FloppyDisk: ~Copyable { } func newDisk() -> FloppyDisk { var result = FloppyDisk() format(&result) return result } func format(_ disk: inout FloppyDisk) { var tempDisk = disk // ... disk = tempDisk }
-
10:28 - Noncopyable BankTransfer
struct BankTransfer: ~Copyable { consuming func run() { // .. do it .. discard self } deinit { cancel() } consuming func cancel() { // .. do the cancellation .. discard self } }
-
11:10 - Schedule function for noncopyable BankTransfer
func schedule(_ transfer: consuming BankTransfer, _ delay: Duration) async throws { if delay < .seconds(1) { transfer.run() return } try await Task.sleep(for: delay) transfer.run() }
-
12:12 - Overview of conformance constraints
struct Command { } protocol Runnable { consuming func run() } extension Command: Runnable { func run() { /* ... */ } } func execute1<T>(_ t: T) {} func execute2<T>(_ t: T) where T: Runnable { t.run() } func test(_ cmd: Command, _ str: String) { execute1(cmd) execute1(str) execute2(cmd) execute2(str) // expected error: 'execute2' requires that 'String' conform to 'Runnable' }
-
15:50 - Noncopyable generics: 'execute' function
protocol Runnable: ~Copyable { consuming func run() } struct Command: Runnable { func run() { /* ... */ } } struct BankTransfer: ~Copyable, Runnable { consuming func run() { /* ... */ } } func execute2<T>(_ t: T) where T: Runnable { t.run() } func execute3<T>(_ t: consuming T) where T: Runnable, T: ~Copyable { t.run() } func test() { execute2(Command()) execute2(BankTransfer()) // expected error: 'execute2' requires that 'BankTransfer' conform to 'Copyable' execute3(Command()) execute3(BankTransfer()) }
-
18:05 - Conditionally Copyable
struct Job<Action: Runnable & ~Copyable>: ~Copyable { var action: Action? } func runEndlessly(_ job: consuming Job<Command>) { while true { let current = copy job current.action?.run() } } extension Job: Copyable where Action: Copyable {} protocol Runnable: ~Copyable { consuming func run() } struct Command: Runnable { func run() { /* ... */ } }
-
19:27 - Extensions of types with noncopyable generic parameters
extension Job { func getAction() -> Action? { return action } } func inspectCmd(_ cmdJob: Job<Command>) { let _ = cmdJob.getAction() let _ = cmdJob.getAction() } func inspectXfer(_ transferJob: borrowing Job<BankTransfer>) { let _ = transferJob.getAction() // expected error: method 'getAction' requires that 'BankTransfer' conform to 'Copyable' } struct Job<Action: Runnable & ~Copyable>: ~Copyable { var action: Action? } extension Job: Copyable where Action: Copyable {} protocol Runnable: ~Copyable { consuming func run() } struct Command: Runnable { func run() { /* ... */ } } struct BankTransfer: ~Copyable, Runnable { consuming func run() { /* ... */ } }
-
20:14 - Cancellable for Jobs with Copyable actions
protocol Cancellable { mutating func cancel() } extension Job: Cancellable { mutating func cancel() { action = nil } }
-
21:00 - Cancellable for all Jobs
protocol Cancellable: ~Copyable { mutating func cancel() } extension Job: Cancellable where Action: ~Copyable { mutating func cancel() { action = nil } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。