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 自己管内存。
相关推荐
njsgcs1 天前
Swift playground 网页刷新切换随机页面的网页查看器WebKit
swift
桃子叔叔3 天前
基于SWIFT框架的预训练微调和推理实战指南之完整实战项目
大模型·swift
菜的不敢吱声3 天前
swift学习第5天
学习·ssh·swift
符哥20083 天前
Swift开发app常见第三方库
学习·swift
初级代码游戏3 天前
iOS开发 SwiftUI 5 : 文本输入 密码输入 多行输入
ios·swiftui·swift
菜的不敢吱声3 天前
swift学习第4天
服务器·学习·swift
菜的不敢吱声5 天前
swift学习第2,3天
python·学习·swift
大熊猫侯佩5 天前
拒绝“假死”:为何上滑关闭是测试大忌?揭秘 iOS 真实 OOM 触发指南
app·swift·apple
大熊猫侯佩5 天前
Swift 6.2 列传(第十六篇):阿朱的“易容术”与阿紫的“毒药测试”
swift·编程语言·apple
麦兜*5 天前
【Swift】苹果App开发全流程解析:从Xcode配置到App Store上架避坑指南
xcode·swift