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 自己管内存。
相关推荐
报错小能手3 小时前
ios开发方向——swift并发进阶核心 Task、Actor、await 详解
开发语言·学习·ios·swift
用户79457223954131 天前
【AFNetworking】OC 时代网络请求事实标准,Alamofire 的前身
objective-c·swift
报错小能手1 天前
SwiftUI 框架 认识 SwiftUI 视图结构 + 布局
ui·ios·swift
东坡肘子1 天前
被 Vibe 摧毁的版权壁垒,与开发者的新护城河 -- 肘子的 Swift 周报 #131
人工智能·swiftui·swift
报错小能手2 天前
ios开发方向——swift错误处理:do/try/catch、Result、throws
开发语言·学习·ios·swift
小夏子_riotous2 天前
openstack的使用——5. Swift服务的基本使用
linux·运维·开发语言·分布式·云计算·openstack·swift
mCell2 天前
MacOS 下实现 AI 操控电脑(Computer Use)的思考
macos·agent·swift
用户79457223954132 天前
【DGCharts】iOS 图表渲染事实标准——8 种图表类型、高度可定制,3 行代码画出一条折线
swiftui·swift
chaoguo12343 天前
Any metadata 的内存布局
swift·metadata·value witness table
tangweiguo030519874 天前
SwiftUI布局完全指南:从入门到精通
ios·swift