Swift 集合类型详解(三):自定义集合、持久化结构与 ORM 共舞

从零实现一个 CircularArray

需求:固定容量,到达上限后从头覆盖,支持 for-incountrandomAccess

步骤:

  1. 遵循 Collection 协议;
  2. 提供 startIndexendIndex、下标;
  3. & 运算做环形回绕。
swift 复制代码
/// 1. 数据容器
public struct CircularArray<Element> {
    private var buffer: [Element?]          // 故意可空,区分"未写入"
    private var head = 0                    // 逻辑起始索引
    private var _count = 0                  // 实际元素数
    public let capacity: Int
    
    public init(capacity: Int) {
        precondition(capacity > 0)
        self.capacity = capacity
        buffer = Array(repeating: nil, count: capacity)
    }
}

/// 2. 遵循 Collection ------ 只读部分
extension CircularArray: Collection {
    public var startIndex: Int { 0 }
    public var endIndex: Int { _count }
    
    public func index(after i: Int) -> Int { i + 1 }
    
    public subscript(position: Int) -> Element {
        get {
            precondition(position < _count, "Index out of range")
            let idx = (head + position) & (capacity - 1)   // 要求 capacity 为 2 的幂
            return buffer[idx]!
        }
        set {
            precondition(position < _count, "Index out of range")
            let idx = (head + position) & (capacity - 1)
            buffer[idx] = newValue
        }
    }
}

/// 3. 支持 RangeReplaceableCollection ------ 可写
extension CircularArray: RangeReplaceableCollection {
    public init() {
        self.init(capacity: 16)
    } // 默认容量
    
    public mutating func replaceSubrange<C>(_ subrange: Range<Int>, with newElements: C) where C : Collection, Element == C.Element {
        guard subrange.upperBound - subrange.lowerBound == newElements.count else { return }
        let lowerBound = subrange.lowerBound
        for i in subrange {
            let ind = i - lowerBound
            self[ind] = newElements[newElements.index(newElements.startIndex, offsetBy: ind)]
        }
    }
    
    /// 追加:满则覆盖最老元素
    public mutating func append(_ newElement: Element) {
        let idx = (head + _count) & (capacity - 1)
        buffer[idx] = newElement
        if _count < capacity {
            _count += 1
        } else {
            head = (head + 1) & (capacity - 1) // 滑动窗口
        }
    }
    
    /// 批量追加
    public mutating func append<S: Sequence>(contentsOf newElements: S) where S.Element == Element {
        for e in newElements { append(e) }
    }
}

验证:

swift 复制代码
var cq = CircularArray<Int>(capacity: 4)
for i in 1...6 { cq.append(i) }
print(Array(cq))          // [3, 4, 5, 6]
cq[0] = 99
print(cq[0])              // 99

让 CircularArray 支持 Set/Dictionary 运算

需求:去重 + 快速查询。

做法:把 CircularArray 当成"有序窗口",外部再包一个 Set 做存在性判断。

swift 复制代码
struct UniqueCircularArray<T: Hashable> {
    private var store = CircularArray<T>(capacity: 100)
    private var set = Set<T>()
    
    mutating func append(_ element: T) {
        if set.contains(element) { return }
        if store.count == store.capacity {        // 窗口已满,先删最老
            let oldest = store[store.startIndex]
            set.remove(oldest)
        }
        store.append(element)
        set.insert(element)
    }
    
    func contains(_ element: T) -> Bool { set.contains(element) }
}

时间复杂度:

  • append 均摊 O(1);
  • contains O(1);
  • 内存占用 O(capacity)。

不可变集合的"持久化"魔法

什么是持久化(Persistent)

  • 修改后旧版本仍可用;
  • 共享未修改节点,节省内存与复制时间;
  • Swift 原生 Array/Set/Dictionary 都是"写时复制",但并非持久化结构------一旦副本被修改,旧版本立即失效。
swift 复制代码
// 简化版 PersistentVector
public struct PersistentVector<Element> {
    private var root: Node?
    private let shift = 5
    private let mask = 0b1_1111
    
    private final class Node {
        var array: [Any?] = Array(repeating: nil, count: 32)
    }
    
    public var count: Int { /* 省略 */ 0 }
    
    public subscript(index: Int) -> Element {
        get { /* 沿树查找 */ fatalError() }
    }
    
    public func appending(_ element: Element) -> PersistentVector {
        var new = self
        // 仅复制受影响的节点
        // ...
        return new
    }
}

优势:

  • 100 万元素,修改一次只分配 < 20 个 Node(约 1 kB);
  • 旧版本仍可安全读,天然支持"时间旅行调试"、"撤销重做"。

劣势:

  • 随机访问常数比 Array 大约 35 倍;
  • 实现复杂,需自己维护,或直接用第三方(SwiftCollectino 、Immer-Swift)。

ORM 与值语义集合:到底怎么存?

SwiftData(iOS 17+)

SwiftData 原生支持 Codable 值类型,可直接存储 Array/Set

swift 复制代码
@Model
final class TodoItem {
    var tags: Set<String> = []          // 自动生成表,多对多拆表
}

注意:

  • 目前不支持自定义 Hashable struct 当 Key;
  • Dictionary 的支持要等到 FoundationDB 层公开。

Realm

Realm 早期只能存 NSArray;Swift-SDK 已支持 List<T>MutableSet<T>,但它们是引用类型。

想把"值语义"带回来:

swift 复制代码
final class Dog: Object {
    @Persisted var name: String
    @Persisted var nicknames: MutableSet<String>
}

// 写入
let dog = Dog()
dog.nicknames.append(objectsIn: ["Buddy", "Max"])

读出来后想转回 Swift 原生:

swift 复制代码
let swiftSet = Set(dog.nicknames)   // 拷贝一次,脱离 Realm 管理

陷阱:

  • MutableSet 只能在写事务里修改;
  • 跨线程无法直接传递,需要 ThreadSafeReference

CoreData

最麻烦:

  • 不支持任何 Swift 原生集合,必须转 Transformable 或建中间实体;
  • Transformable 底层是 NSKeyedArchiver,性能差且无法查询;
  • 推荐:把集合拍平成 NSData 或 JSON String,存成 Derived Attribute。

拍平(Flatten)与查询(Query)实战

需求:存一个"按天分组的待办" -> [Date: [Todo]]

方案:

  1. 建中间实体 DayTodoGroup
swift 复制代码
@Model
final class DayTodoGroup {
    var date: Date
    var todos: [Todo] = []        // SwiftData 原生支持数组嵌套
}
  1. 查询某天的数据:
swift 复制代码
let predicate = #Predicate<DayTodoGroup> { $0.date.startOfDay == target.startOfDay }
let group = modelContext.fetch(FetchDescriptor(predicate: predicate)).first
  1. 如果想"模糊查询所有含 tag = 工作"的待办:
    • tags: Set<String> 存在 Todo 里;
    • SwiftData 会自动生成多对多关联表,支持 ANY tags == "工作" 谓词。

线程安全与并发

  • Swift 原生集合是值语义,单线程绝对安全;
  • 多线程同时读没问题;
  • 多线程写同一块内存会触发"数据竞争" → 用 actorDispatchQueue 保护:
swift 复制代码
actor SafeCircularArray<T: Hashable> {
    private var store = UniqueCircularArray<T>()
    
    func append(_ element: T) { store.append(element) }
    func contains(_ element: T) -> Bool { store.contains(element) }
}

SwiftData/Realm 的集合对象必须在对应线程/队列使用,不能跨并发域传递。

最佳实践 10 条

  1. 95% 场景用原生 Array/Set/Dictionary,不要过早优化。
  2. 需要"滑动窗口"去重用 Array + Set 双持,别自己写链表。
  3. 大容量(>10 万)先 reserveCapacity,再批量添加。
  4. 自定义 Hashable 一定只哈希"不变主键",class 记得加 final 防继承破坏。
  5. 对"撤销/重做"或"函数式"需求,才上持久化结构;否则 Array 足够。
  6. SwiftData 能存 Set/Array,就别转 JSON String;查询性能高一个量级。
  7. Realm 集合是引用类型,读多写少用 ThreadSafeReference,写多读少直接事务。
  8. CoreData 新项目建议直接迁移到 SwiftData;老项目用拍平 + Derived Attribute。
  9. 多线程共享集合,用 actor 包一层,比锁简洁。
  10. 真·极限性能(音视频缓冲、游戏对象池)再用 UnsafePointer 自己管内存。
相关推荐
HarderCoder3 小时前
Swift 集合类型详解(一):Array、Set、Dictionary 全貌与选型思路
swift
HarderCoder3 小时前
Swift 集合类型详解(二):自定义 Hashable、值语义与性能陷阱
swift
东坡肘子7 小时前
Sora 2:好模型,但未必是好生意 | 肘子的 Swift 周报 #0105
人工智能·swiftui·swift
HarderCoder18 小时前
Swift 6 并发深渊:@unchecked Sendable 与“隐式 MainActor”如何合谋杀死你的 App
swiftui·swift
HarderCoder19 小时前
告别 UIKit 生命周期:SwiftUI 视图一生全解析——从 init 到 deinit 的“隐秘角落”
swiftui·swift
HarderCoder20 小时前
Swift 中的基本运算符:从加减乘除到逻辑与或非
ios·swift
HarderCoder20 小时前
Swift 中“特性开关”实战笔记——用编译条件+EnvironmentValues优雅管理Debug/TestFlight/AppStore三环境
ios·swift
HarderCoder20 小时前
Swift 并发任务中到底该不该用 `[weak self]`?—— 从原理到实战一次讲透
ios·swift
大熊猫侯佩1 天前
天网代码反击战:Swift 三元运算符的 “一行破局” 指南
swiftui·swift·apple