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 保護 🔒
- 編譯器檢查 → 提前發現問題 🛡️