swift-actor-persistence

swift-actor-persistence

熱門

使用 Swift Actor 實現執行緒安全的資料持久化 — 記憶體快取搭配檔案儲存,從設計上消除資料競爭。

23萬星標
3.5萬分支
更新於 2026/7/17
SKILL.md
readonlyread-only
name
swift-actor-persistence
description

使用 Swift Actor 實現執行緒安全的資料持久化 — 記憶體快取搭配檔案儲存,從設計上消除資料競爭。

Swift Actors 實現執行緒安全的持久化

使用 Swift actors 建立執行緒安全的資料持久層。結合記憶體快取與檔案儲存,利用 actor 模型在編譯時期消除資料競爭。

使用時機

  • 在 Swift 5.5+ 中建立資料持久層
  • 需要對共享可變狀態進行執行緒安全存取
  • 想要消除手動同步(鎖、DispatchQueues)
  • 建構離線優先的應用程式,需要本地儲存

核心模式

基於 Actor 的 Repository

Actor 模型保證序列化存取 — 編譯器強制執行,無資料競爭。

public actor LocalRepository<T: Codable & Identifiable> where T.ID == String {
    private var cache: [String: T] = [:]
    private let fileURL: URL

    public init(directory: URL = .documentsDirectory, filename: String = "data.json") {
        self.fileURL = directory.appendingPathComponent(filename)
        // 在 init 中同步載入(actor 隔離尚未生效)
        self.cache = Self.loadSynchronously(from: fileURL)
    }

    // MARK: - 公開 API

    public func save(_ item: T) throws {
        cache[item.id] = item
        try persistToFile()
    }

    public func delete(_ id: String) throws {
        cache[id] = nil
        try persistToFile()
    }

    public func find(by id: String) -> T? {
        cache[id]
    }

    public func loadAll() -> [T] {
        Array(cache.values)
    }

    // MARK: - 私有方法

    private func persistToFile() throws {
        let data = try JSONEncoder().encode(Array(cache.values))
        try data.write(to: fileURL, options: .atomic)
    }

    private static func loadSynchronously(from url: URL) -> [String: T] {
        guard let data = try? Data(contentsOf: url),
              let items = try? JSONDecoder().decode([T].self, from: data) else {
            return [:]
        }
        return Dictionary(uniqueKeysWithValues: items.map { ($0.id, $0) })
    }
}

使用方式

由於 actor 隔離,所有呼叫自動為非同步:

let repository = LocalRepository<Question>()

// 讀取 — 從記憶體快取快速 O(1) 查詢
let question = await repository.find(by: "q-001")
let allQuestions = await repository.loadAll()

// 寫入 — 更新快取並原子性地寫入檔案
try await repository.save(newQuestion)
try await repository.delete("q-001")

結合 @Observable ViewModel

@Observable
final class QuestionListViewModel {
    private(set) var questions: [Question] = []
    private let repository: LocalRepository<Question>

    init(repository: LocalRepository<Question> = LocalRepository()) {
        self.repository = repository
    }

    func load() async {
        questions = await repository.loadAll()
    }

    func add(_ question: Question) async throws {
        try await repository.save(question)
        questions = await repository.loadAll()
    }
}

關鍵設計決策

決策 理由
Actor(而非 class + lock) 編譯器強制執行緒安全,無需手動同步
記憶體快取 + 檔案持久化 從快取快速讀取,寫入磁碟持久儲存
init 中同步載入 避免非同步初始化的複雜性
以 ID 為鍵的字典 依識別碼 O(1) 查詢
泛型約束 Codable & Identifiable 可重用於任何模型類型
原子性檔案寫入(.atomic 防止當機時部分寫入

最佳實踐

  • 使用 Sendable 類型 處理所有跨越 actor 邊界的資料
  • 保持 actor 的公開 API 最小化 — 只暴露領域操作,而非持久化細節
  • 使用 .atomic 寫入 防止應用程式在寫入中途當機導致資料損毀
  • init 中同步載入 — 非同步初始化對本地檔案而言增加複雜性但好處有限
  • 結合 @Observable ViewModel 實現反應式 UI 更新

應避免的反模式

  • 在新的 Swift 並行程式中使用 DispatchQueueNSLock 而非 actors
  • 將內部快取字典暴露給外部呼叫者
  • 讓檔案 URL 可設定但未經驗證
  • 忘記所有 actor 方法呼叫都需要 await — 呼叫端必須處理非同步上下文
  • 使用 nonisolated 繞過 actor 隔離(違背設計目的)

使用時機

  • iOS/macOS 應用程式的本地資料儲存(使用者資料、設定、快取內容)
  • 離線優先架構,稍後與伺服器同步
  • 任何應用程式中多個部分同時存取的共享可變狀態
  • 用現代 Swift 並行程式取代舊有的 DispatchQueue 執行緒安全機制