在值类型篇中,我们将 Swift 的默认行为概括为“复制”:赋值会复制,传入函数也会复制。
本文接续上一篇文章 Swift 深入 #7。
但如果有些值不能被复制呢?比如文件句柄、互斥锁、银行转账令牌等“只有世界上唯一一个才有意义”的资源。
本期深入系列的主题是所有权(ownership)。Swift 5.9 的 ~Copyable(不可复制类型)、borrowing 和 consuming 为此打开了大门。SE-0390: Noncopyable Structs and Enums
了解 Rust 的读者应该会觉得熟悉。没错,Swift 以自己的方式引入了 Rust 的核心思想。
不过方向不同。Rust 默认采用所有权且没有例外,而 Swift 默认采用复制,所有权只是可选工具。
复制默认世界中的漏洞
Swift 的所有类型默认都是 Copyable。即使没有显式声明,编译器也会让它隐式遵循该协议。
值类型篇中介绍的赋值与传递复制语义就来源于此。对于大多数值(数字、字符串、坐标),这是完美的默认行为。
问题在于表示资源的类型。想象一个封装文件描述符的 struct。
struct FileHandle {
let fd: Int32
func close() { /* fd 关闭 */ }
}
let a = FileHandle(fd: open("data.txt"))
let b = a // 复制——现在两个人都知道同一个 fd
a.close()
b.close() // 再次关闭已关闭的 fd——未定义行为
句柄被复制的瞬间,“谁负责关闭它”就变得模糊。重复释放、使用已关闭句柄等资源错误,根源全是这种未经允许的复制。
过去的常规做法是使用类,并在 deinit 中关闭,通过一个引用统一管理主体。
它确实有效,但需要付出 ARC(Automatic Reference Counting,自动引用计数)篇中提到的成本,也就是堆和引用计数。
而且编译器仍然不知道“不能复制”本身是一条规则。
~Copyable——将禁止复制写入类型
Swift 5.9 的答案是不可复制类型(Swift Evolution 提案 SE-0390)。带波浪号的 ~Copyable 声明“不遵循 Copyable”。SE-0390: Noncopyable Structs and Enums
struct FileHandle: ~Copyable {
let fd: Int32
consuming func close() { /* fd 关闭 */ }
deinit { /* 如果尚未关闭,就在这里关闭 */ }
}
let a = FileHandle(fd: open("data.txt"))
let b = a // 不是复制,而是移动——所有权转移到 b
// print(a.fd) // 编译错误: a已经被消费
行为发生了根本变化。赋值不再是复制,而是移动(move);转移所有权的变量从那一刻起禁止使用。
违反规则不会导致运行时崩溃,而是编译错误。编译器像记账一样管理“这个值始终只有一个所有者”的规则。
就像 Optional 将 nil、Sendable 将竞争转化为类型问题,资源的唯一性也变成了类型问题。
此外,struct 可以声明 deinit,因此所有者离开作用域时会执行确定性的清理代码。
无需类也能实现 RAII(Resource Acquisition Is Initialization)风格的资源管理。
borrowing 与 consuming——向函数传递值的三种方式
尝试向函数传递不可复制值时,会出现新的问题。既然不能复制,就必须决定是借出还是转移。
参数修饰符就是这种声明。
**borrowing 表示借用。**函数只读取值,所有权仍保留在调用方。
函数返回后,调用方仍可继续使用该值。func checksum(of handle: borrowing FileHandle) -> Int适合这类只读操作。
**consuming 表示转移。**所有权转给函数,调用方不能再使用该值。上面的 consuming func close() 正是这个含义。
调用 close 后再使用句柄会变成编译错误,“再次使用已关闭句柄”这一类错误从语法层面消失了。
它也适合建模银行转账令牌、一次性票据等“使用后就应消失”的领域概念。
**inout 保持原有方式,即借出后修改。**三者并列,就构成了函数参数的所有权词汇:只读(borrowing)、取走(consuming)、修改(inout)。
这些修饰符也可以用于 Copyable 类型。此时它们是性能提示,而不是语义要求。
它们用借用和移动替代默认约定可能产生的 retain/release 或复制,以减少 ARC 流量。使用 consume 运算符(let b = consume a)要求显式移动,也属于同一类。
不过,这是必须先进行测量的微优化领域。关于分派便利性的警告在这里同样适用。
实践判断——在哪里使用,在哪里不使用
准确理解这一功能目前的位置非常重要。
**它适合资源的唯一所有权。**例如文件和套接字句柄包装器、锁令牌、事务保护器以及硬件访问权限。
事实上,Embedded Swift 是这一功能的主要推动力之一。在堆和 ARC 都成本高昂的微控制器环境中,需要一种无需类也能安全管理资源的工具。
标准库 Mutex 传递的值,以及 Span 这样的新类型,都属于这一脉络。
**普通应用代码的默认值仍然是 Copyable struct。**面向值的原则没有改变。
预先对领域数据应用 ~Copyable 属于过度设计。它与泛型生态仍有摩擦:不可复制类型不能直接与假设 Copyable 的现有泛型和集合混用,语言还在逐步解决这一问题。
目前,应将它作为专用工具,只用于能够对“复制它会是错误吗?”回答“是”的类型。
**最后用 Rust 做个比较,**Rust 是“所有权默认”的语言,对所有值强制执行所有权规则,甚至要求生命周期注解。
Swift 保留复制默认的世界,只将需要的类型转移到所有权世界,采用“选择性所有权”。
这是 Progressive Disclosure 哲学的典型。不了解所有权的开发者,其代码中完全不会出现这一概念;只有需要的人才会打开下一层。
总结
- 所有类型都隐式为 Copyable,而资源类型的未经允许复制是重复释放类错误的根源。
- ~Copyable(SE-0390)禁止复制,将赋值变为移动,并让使用已消费变量成为编译错误。struct 允许 deinit,因此也能进行确定性清理。
- 参数所有权词汇:borrowing(只读、借用)、consuming(取走、转移)、inout(修改)。consuming 方法将“使用后消失”的含义写入语法。
- 适用于资源的唯一所有权(句柄、锁、令牌、嵌入式环境);普通数据的默认值仍是 Copyable struct。不同于 Rust 的全面所有权,Swift 采用选择性所有权。
下一篇既是深入系列也是整个 Swift 系列的最后主题:揭开“Swift 5 中应用体积突然变小”事件的始末——ABI(Application Binary Interface)稳定性。SE-0390: Noncopyable Structs and Enums
来源与确认标准
- SE-0390: Noncopyable Structs and Enums — Swift Evolution · 标准与规范原文 · 确认日期 2026-08-17 · 依据:Swift 5.9 的 ~Copyable、consuming・borrowing 与不可复制类型规则

![[Swift 深入 #8] ~Copyable 与所有权、禁止复制的类型 封面图](/assets/images/posts/d9a5f7c8-fa35-4590-aca1-b3e4e60f6a6b/swift-noncopyable-ownership-move.jpg)