Swift 与 Objective-C

[Swift 深入 #8] ~Copyable 与所有权、禁止复制的类型

文件句柄或锁这类只有保持唯一才有意义的资源不应被复制。本文介绍如何用 Swift 5.9 的 ~Copyable 将禁止复制写入类型,并整理 borrowing、consuming、inout 三种传递方式及其适用场景。

7 分钟阅读
[Swift 深入 #8] ~Copyable 与所有权、禁止复制的类型 封面图

在值类型篇中,我们将 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 哲学的典型。不了解所有权的开发者,其代码中完全不会出现这一概念;只有需要的人才会打开下一层。

将 borrowing、consuming、inout 三种传递方式比作三个窗口的示意图
只读(borrowing)、取走(consuming)、修改(inout)

总结

  • 所有类型都隐式为 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 与不可复制类型规则

延伸阅读