从零实现一个 CircularArray
需求:固定容量,到达上限后从头覆盖,支持 for-in
、count
、randomAccess
。
步骤:
- 遵循
Collection
协议; - 提供
startIndex
、endIndex
、下标; - 用
&
运算做环形回绕。
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]]
方案:
- 建中间实体
DayTodoGroup
:
swift
@Model
final class DayTodoGroup {
var date: Date
var todos: [Todo] = [] // SwiftData 原生支持数组嵌套
}
- 查询某天的数据:
swift
let predicate = #Predicate<DayTodoGroup> { $0.date.startOfDay == target.startOfDay }
let group = modelContext.fetch(FetchDescriptor(predicate: predicate)).first
- 如果想"模糊查询所有含 tag = 工作"的待办:
- 把
tags: Set<String>
存在Todo
里; - SwiftData 会自动生成多对多关联表,支持
ANY tags == "工作"
谓词。
- 把
线程安全与并发
- Swift 原生集合是值语义,单线程绝对安全;
- 多线程同时读没问题;
- 多线程写同一块内存会触发"数据竞争" → 用
actor
或DispatchQueue
保护:
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 条
- 95% 场景用原生 Array/Set/Dictionary,不要过早优化。
- 需要"滑动窗口"去重用 Array + Set 双持,别自己写链表。
- 大容量(>10 万)先
reserveCapacity
,再批量添加。 - 自定义 Hashable 一定只哈希"不变主键",class 记得加
final
防继承破坏。 - 对"撤销/重做"或"函数式"需求,才上持久化结构;否则 Array 足够。
- SwiftData 能存 Set/Array,就别转 JSON String;查询性能高一个量级。
- Realm 集合是引用类型,读多写少用
ThreadSafeReference
,写多读少直接事务。 - CoreData 新项目建议直接迁移到 SwiftData;老项目用拍平 + Derived Attribute。
- 多线程共享集合,用
actor
包一层,比锁简洁。 - 真·极限性能(音视频缓冲、游戏对象池)再用 UnsafePointer 自己管内存。