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 等新型別,都屬於這個脈絡。

**一般 App 程式碼的預設仍是 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 中 App 容量突然變小」事件的始末——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 與不可複製型別規則

延伸閱讀