Skip to main content

Swift Sendable 協議完整

在 Swift Concurrency 中安全地跨執行緒傳遞資料

📖 什麼是 Sendable?

Sendable 是 Swift 5.5+ 引入的協議,用於確保型別的值能夠安全地在不同的 actor並行任務間傳遞,避免資料競爭(data race)。

核心概念

  • 編譯期檢查:Swift 在編譯時檢查型別是否安全
  • 跨執行緒安全:確保資料在並行環境中不會被意外修改
  • 自動推斷:許多型別自動符合 Sendable

✅ 自動符合 Sendable 的型別

值型別(Value Types)

// 基本型別
let number: Int = 42
let text: String = "Hello"
let flag: Bool = true

// 集合型別(元素也必須是 Sendable)
let numbers: [Int] = [1, 2, 3]
let dict: [String: Int] = ["a": 1, "b": 2]

自定義 Struct/Enum

// ✅ 安全的 struct
struct User: Sendable {
let id: Int
let name: String
let email: String
}

// ✅ 安全的 enum
enum Status: Sendable {
case active
case inactive
case pending(String) // String 是 Sendable
}

⚠️ 需要特別注意的型別

Class(參考型別)

// ❌ 預設不安全
class Counter {
var value: Int = 0 // 可變狀態
}

// ❌ 編譯錯誤:'Counter' 不符合 'Sendable'
// Task {
// let counter = Counter()
// // 無法傳遞到其他任務
// }

解決方案

1. 使用 @unchecked Sendable

final class SafeCounter: @unchecked Sendable {
private let queue = DispatchQueue(label: "counter.queue")
private var _value: Int = 0

var value: Int {
queue.sync { _value }
}

func increment() {
queue.sync { _value += 1 }
}
}

2. 使用 Actor

actor CounterActor {
private var value: Int = 0

func getValue() -> Int {
return value
}

func increment() {
value += 1
}
}

3. 使用 @MainActor

@MainActor
class UIViewController {
var title: String = ""
// 所有操作都在主執行緒
}

🚀 實際應用範例

1. 網路請求

struct APIResponse: Sendable {
let data: Data
let statusCode: Int
}

func fetchData() async throws -> APIResponse {
// 可以安全地跨任務傳遞
return try await withCheckedThrowingContinuation { continuation in
// 網路請求邏輯
}
}

2. 資料模型

struct UserProfile: Sendable {
let id: UUID
let username: String
let avatarURL: URL?
let createdAt: Date
}

// 可以安全地在並行任務中使用
Task {
let profile = UserProfile(...)
await updateUI(with: profile)
}

3. 設定物件

struct AppConfig: Sendable {
let apiBaseURL: String
let timeout: TimeInterval
let retryCount: Int
}

// 全域設定可以安全共享
let config = AppConfig(
apiBaseURL: "https://api.example.com",
timeout: 30.0,
retryCount: 3
)

📋 Sendable 速查表

型別是否自動 Sendable說明
Int, String, Bool基本型別
Array<T>, Dictionary<K,V>當 T, K, V 是 Sendable 時
struct當所有成員是 Sendable 時
enum當所有關聯值是 Sendable 時
class預設不安全,需要手動標記
closure需要 @Sendable 標記
actor天然安全

🛠️ 最佳實踐

1. 優先使用值型別

// ✅ 推薦
struct UserData: Sendable {
let name: String
let age: Int
}

// ❌ 避免
class UserData {
var name: String
var age: Int
}

2. 使用 @Sendable 標記閉包

func processData(_ data: [Int], completion: @Sendable @escaping (Int) -> Void) {
Task {
let result = data.reduce(0, +)
completion(result)
}
}

3. 小心處理可變狀態

// ✅ 使用 actor 保護可變狀態
actor DataStore {
private var cache: [String: Any] = [:]

func set(_ value: Any, for key: String) {
cache[key] = value
}

func get(for key: String) -> Any? {
return cache[key]
}
}

🔍 常見錯誤與解決方案

錯誤 1:Class 不符合 Sendable

// ❌ 錯誤
class MyClass {
var value: Int = 0
}

// ✅ 解決方案
final class MyClass: @unchecked Sendable {
private let queue = DispatchQueue(label: "myclass.queue")
private var _value: Int = 0

var value: Int {
queue.sync { _value }
}
}

錯誤 2:閉包捕獲非 Sendable 值

// ❌ 錯誤
class ViewController {
var data: [String] = []

func processData() {
Task {
// 無法捕獲 self.data
let result = data.map { $0.uppercased() }
}
}
}

// ✅ 解決方案
class ViewController {
var data: [String] = []

func processData() {
let localData = data // 複製到本地變數
Task {
let result = localData.map { $0.uppercased() }
}
}
}

💡 記憶口訣

  • 值型別 → 自動安全 ✅
  • 參考型別 → 需要手動標記 ⚠️
  • 可變狀態 → 使用 actor 保護 🔒
  • 編譯器檢查 → 提前發現問題 🛡️

📚 相關資源