Highlight
Apple 发布 Swift Algorithms 和 Swift Collections 两个开源包,让开发者现在就能使用新的序列算法、双端队列、有序集合和有序字典,并把这些 API 作为未来标准库能力的孵化场。
核心内容
一个聊天 App 经常有这种代码:用户选中几行消息,你要拿到对应的 Message;用户点开图片,你要从消息里收集附件;详情页要显示最近 6 张照片。最直接的写法是 for 循环、临时数组、if let、break。
这些循环能工作,但它们把意图藏在控制流里。读代码的人需要看完整个循环,才能知道它是在转换、过滤、展平,还是只取前几个元素。循环还容易漏掉容量预留、延迟计算这些性能细节。
Swift 标准库已经有 map、compactMap、flatMap、prefix、suffix、reversed 这些算法。Swift Algorithms 继续补齐标准库还没有覆盖的模式,例如滑动窗口、相邻元素、分块、按条件分组。
Apple 同时发布 Swift Collections。Swift 标准库长期只有三类通用数据结构:Array、Set、Dictionary。它们适合做通用边界类型,但队列、有序唯一列表、有序键值表这些场景,需要更贴近问题的数据结构。
Swift Collections 提供 Deque、OrderedSet、OrderedDictionary。它们保持 Swift 集合的值语义和 copy-on-write 行为,同时补上标准库缺少的操作模型。
从原始循环走向算法词汇
聊天 App 的代码里,很多循环其实都有名字。选中行转消息,是 map。收集非空附件,是 compactMap。一条消息生成多个 transcript item 后再展平,是 flatMap。
有了这些名字,代码的重心从“怎么循环”变成“这一步在做什么”。这也是 Swift Algorithms 的定位:把缺失的常见模式先放进开源包,开发者现在使用,社区再一起验证它们是否适合进入标准库。
从通用集合走向合适的数据结构
Array 很适合按下标访问,但在开头插入元素需要移动已有元素。队列、任务调度、聊天草稿撤销栈这类场景,经常从头尾两端插入和移除,Array 的成本偏高。
Set 保证唯一性,但不保证顺序。待办列表、标签列表、用户手动排序的收藏夹,需要同时满足“元素唯一”和“顺序稳定”。这就是 OrderedSet 的位置。
Dictionary 可以按 key 快速查值,但键值对顺序没有作为核心语义暴露。需要按插入顺序展示字段、参数、配置项时,OrderedDictionary 把 key 查找和顺序保存放在同一个类型里。
详细内容
标准库算法:把循环改写成可读的操作
(01:00)map 用来把输入集合里的每个元素转换成另一个值。演讲中的例子来自表格选中行:indexPathsForSelectedRows 里存的是 index path,App 需要拿到对应的消息。
// Raw loop:
var selectedMessages: [Message] = []
for indexPath in indexPathsForSelectedRows {
selectedMessages.append(messages[indexPath.row])
}
// Using `map` makes this clearer and faster.
indexPathsForSelectedRows.map { messages[$0.row] }
关键点:
selectedMessages是原始循环里的临时数组。for indexPath in indexPathsForSelectedRows逐个访问被选中的行。messages[indexPath.row]根据行号取出对应的Message。map直接表达“把每个 index path 转成 message”。- 演讲指出,
map会预留容量,避免原始循环中数组扩容带来的中间分配。
(01:36)compactMap 处理“过滤 nil 并解包”的常见模式。聊天 App 里,消息可能有附件,也可能没有附件。
// Raw loop:
var attachments: [Attachment] = []
for message in messages {
if let attachment = message.attachment {
attachments.append(attachment)
}
}
// The above is just a `map` and a `filter`.
messages
.filter { $0.attachment != nil }
.map { $0.attachment! }
// This pattern is so common we have a special name and algorithm for it.
messages.compactMap { $0.attachment }
关键点:
attachments收集所有存在的附件。if let attachment = message.attachment只处理非空附件。filter先筛掉附件为空的消息。map再把可选附件强制解包。compactMap把筛选和解包合成一步,避免手写!。
(02:06)flatMap 处理“一对多转换”。一条消息可能生成多个 transcript item,如果先 map,结果会变成数组套数组。
extension Message {
func makeMessageParts() -> [TranscriptItem]
}
messages // [Message]
.map { $0.makeMessageParts() } // [[TranscriptItem]]
.joined() // [TranscriptItem]
// This pattern is so common that we have another special kind of map for it.
messages // [Message]
.flatMap { $0.makeMessageParts() } // [TranscriptItem]
关键点:
makeMessageParts()把一条Message拆成多个TranscriptItem。map的结果是[[TranscriptItem]],每条消息对应一个内部数组。joined()把内部数组连接成一个平坦集合。flatMap直接表达“转换后展平”。
Lazy adapter:让算法链按需计算
(03:00)详情页只需要最近 6 张照片。原始循环会反向遍历、判断类型、计数、到 6 个后退出。算法链可以把这些步骤拆开。
// Raw loop:
var photos: [PhotoItem] = []
for item in transcript.reversed() {
if let photo = item as? PhotoItem {
photos.append(photo)
if photos.count == 6 {
break
}
}
}
// The above can be expressed more concisely by chaining together algorithms.
transcript
.reversed() // [TranscriptItem]
.compactMap { $0 as? PhotoItem } // [PhotoItem]
.prefix(6) // [PhotoItem]
// This gives us more flexibility to express this code more clearly.
transcript
.compactMap { $0 as? PhotoItem } // [PhotoItem]
.suffix(6) // [PhotoItem]
.reversed() // [PhotoItem]
关键点:
transcript.reversed()从新到旧遍历。compactMap { $0 as? PhotoItem }只保留照片项,并完成类型转换。prefix(6)取前 6 个照片项。- 另一种写法先取全部照片的
suffix(6),再reversed(),更贴近“最近 6 张照片再倒序展示”的思路。
(04:19)算法链不一定每一步都创建数组。joined() 返回 FlattenSequence,这是 lazy adapter(惰性适配器),创建成本很低,元素按需处理。
extension Message {
func makeMessageParts() -> [TranscriptItem]
}
messages
.map { $0.makeMessageParts() } // [[TranscriptItem]]
.joined() // FlattenSequence<[[TranscriptItem]]>
关键点:
map生成每条消息对应的 item 数组。joined()返回FlattenSequence。FlattenSequence包装底层集合,按访问需求展平元素。- 演讲把这类包装称为 lazy adapter。
(04:58)对带闭包的算法加 .lazy,可以让 map、filter、compactMap 这类操作延迟执行。
transcript
.lazy // LazySequence<[TranscriptItem]>
.compactMap { $0 as? PhotoItem } // LazyCompactMap<[TranscriptItem], PhotoItem>
.suffix(6) // LazyCompactMap<ArraySlice<TranscriptItem>, PhotoItem>
.reversed() // ReversedCollection<LazyCompactMap<ArraySlice<TranscriptItem>, PhotoItem>>
关键点:
.lazy把后续算法链切换到按需计算模式。compactMap不再立即生成照片数组。suffix(6)只关心最后 6 个结果。reversed()返回反向视图。
(05:48)如果最终确实需要数组,用 Array(...) 收集结果。演讲强调,对于会被反复遍历的 transcript,最后保存为数组更合适,避免每次进入编辑模式、点图、打开详情页都重新计算。
Array(
transcript
.lazy
.compactMap { $0 as? PhotoItem }
.suffix(6)
.reversed()
)
关键点:
Array(开始把惰性结果物化为数组。transcript.lazy让中间步骤按需执行。compactMap过滤并转换照片项。suffix(6)取最后 6 张照片。reversed()调整展示顺序。
Swift Algorithms:窗口、相邻元素和分块
(07:13)Swift Algorithms 提供 windows(ofCount:)。它返回滑动窗口,每个窗口是底层集合的 subsequence(子序列),演讲中例子里是 ArraySlice,避免中间分配。
import Algorithms
let x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
for window in x.windows(ofCount: 3) {
print(window)
}
// Prints [0, 1, 2]
// Prints [1, 2, 3]
// Prints [2, 3, 4]
// Prints [3, 4, 5]
关键点:
import Algorithms引入 Swift Algorithms 包。x是待遍历数组。windows(ofCount: 3)生成长度为 3 的重叠窗口。window是原集合的连续切片。- 相邻窗口共享元素,例如
[0, 1, 2]后面是[1, 2, 3]。
(07:30)长度为 2 的窗口很常见,所以包里提供 adjacentPairs()。它返回元组,访问前后两个元素更直接。
import Algorithms
let x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
for (prev, next) in x.adjacentPairs() {
print((prev, next))
}
// Prints (0, 1)
// Prints (1, 2)
// Prints (2, 3)
// Prints (3, 4)
关键点:
adjacentPairs()遍历相邻元素对。(prev, next)直接解构前一个和后一个元素。- 适合比较相邻值,例如检测日期间隔、趋势变化、连续重复。
(07:45)chunks(ofCount:) 按固定大小分组。它和窗口不同,分块之间不重叠。
import Algorithms
let x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
for chunk in x.chunks(ofCount: 3) {
print(chunk)
}
// Prints [0, 1, 2]
// Prints [3, 4, 5]
// Prints [6, 7, 8]
// Prints [9]
关键点:
chunks(ofCount: 3)每 3 个元素生成一组。- 每个
chunk是底层集合的子序列。 - 最后一组不够 3 个元素时,会包含剩余元素。
(08:49)chunked 也可以用自定义条件分组。演讲用它实现聊天记录的时间戳插入:相邻 transcript item 间隔小于一小时,就放在同一组;组与组之间插入 DateItem。
import Algorithms
extension Message {
func makeMessageParts() -> [TranscriptItem]
}
transcript = Array(
messages
.lazy
.flatMap { $0.makeMessageParts() }
.chunked { $1.date.timeIntervalSince($0.date) < 60 * 60 }
.joined { DateItem(date: $1.first!.date) }
)
关键点:
messages.lazy延迟处理消息数组。flatMap { $0.makeMessageParts() }把消息转换成平坦的 transcript item 流。chunked { ... }接收相邻两个元素$0和$1。$1.date.timeIntervalSince($0.date) < 60 * 60表示两条 item 的间隔小于一小时。joined { DateItem(date: $1.first!.date) }在相邻分组之间插入基于下一组首个日期生成的时间戳。Array(...)保存最终 transcript,避免后续重复计算。
Swift Collections:Deque 适合两端操作
(14:56)Deque 是 double-ended queue(双端队列)。它支持从尾部追加、从头部移除,也支持从头部插入、从尾部移除。
import Collections
var queue: Deque = ["A", "B", "C"]
queue.append("D")
queue.append("E")
queue.removeFirst() // "A"
queue.removeFirst() // "B"
queue.prepend("F")
queue.prepend("G")
queue.removeLast() // "E"
queue.removeLast() // "D"
关键点:
import Collections引入 Swift Collections 包。var queue: Deque = ["A", "B", "C"]用数组字面量创建双端队列。append从队尾加入元素。removeFirst从队头移除元素。prepend从队头加入元素。removeLast从队尾移除元素。
(15:46)Deque 的 API 接近 Array。它符合 RandomAccessCollection、MutableCollection 和 RangeReplaceableCollection,可以按整数下标访问、修改、插入范围。
import Collections
var items: Deque = ["D", "E", "f"]
print(items[1]) // "E"
items[2] = "F"
items.insert(contentsOf: ["A", "B", "C"], at: 0)
print(items[1]) // "B"
关键点:
items[1]按位置读取元素。items[2] = "F"通过下标修改元素。insert(contentsOf:at:)在指定位置插入一段元素。at: 0表示插入到开头。- 演讲说明,
Deque会让存储缓冲区绕过边界,因此开头插入不需要移动已有元素,时间近似常量。
(18:39)Deque 在中间删除范围时,可以选择移动前半段或后半段元素来填补空洞。演讲给出的结论是,随机位置删除平均快两倍。
import Collections
var items: Deque = ["A", "B", "C", "D", "E", "F"]
items.removeSubrange(1 ..< 3)
关键点:
items初始包含 6 个元素。1 ..< 3表示下标 1 到下标 3 之前的范围。removeSubrange删除"B"和"C"。Deque可以选择移动更少的一侧来关闭删除后的空洞。
OrderedSet:唯一性和顺序同时成立
(19:33)标准 Set 保证元素唯一,但元素顺序没有意义。两份包含相同元素的集合,打印顺序可能不同,比较结果仍然相等。
let first: Set = ["A", "B", "C", "D", "E", "F"]
print(first) // ["B", "E", "C", "F", "D", "A"]
let second: Set = ["A", "B", "C", "D", "E", "F"]
print(second) // ["A", "D", "E", "F", "C", "B"]
print(first == second) // true
关键点:
Set只关心成员是否相同。print(first)和print(second)的顺序可能不同。first == second仍然是true。- 这种模型适合只需要去重的场景。
(20:26)OrderedSet 保存插入顺序。顺序参与相等性比较;如果只关心成员集合,可以使用 unordered 视图。
import Collections
let first: OrderedSet = ["A", "B", "C", "D", "E", "F"]
print(first) // ["A", "B", "C", "D", "E", "F"]
let second: OrderedSet = ["F", "E", "D", "C", "B", "A"]
print(first == second) // false
print(first.unordered == second.unordered) // true
关键点:
OrderedSet可以用数组字面量创建。print(first)保留字面量中的顺序。first == second比较成员和顺序。unordered视图忽略顺序,只比较成员。
(21:04)OrderedSet 像数组一样支持整数下标、排序和洗牌,也像集合一样避免重复元素。
import Collections
var items: OrderedSet = ["E", "D", "C", "B", "A"]
items[3] // "B"
items.append("F") // (inserted: true, index: 5)
items.insert("B", at: 1) // (inserted: false, index: 3)
items.remove("E")
items.sort()
items.shuffle()
关键点:
items[3]按位置访问元素。append("F")插入新元素,并返回是否插入以及元素下标。insert("B", at: 1)发现"B"已存在,所以返回已有下标。remove("E")按元素删除。sort()和shuffle()执行重排序操作。
OrderedDictionary:按 key 查找,也保留键值对顺序
(26:46)OrderedDictionary 是有序版字典。它用 key 查 value,同时维护键值对的确定顺序,默认是 key 的插入顺序。
import Collections
var dict: OrderedDictionary = [2: "two", 1: "one", 0: "zero"]
print(dict[1]) // Optional("one")
print(dict) // [2: "two", 1: "one", 0: "zero"]
dict[3] = "three"
dict[1] = nil
print(dict) // [2: "two", 0: "zero", 3: "three"]
关键点:
OrderedDictionary可以用字典字面量创建。dict[1]按 key 读取 value。print(dict)展示确定的键值对顺序。dict[3] = "three"为新 key 追加元素。dict[1] = nil删除已有 key。
(27:38)OrderedDictionary 的下标永远表示 key 下标。按位置访问键值对,需要走 elements 视图。
import Collections
var dict: OrderedDictionary = [2: "two", 0: "zero", 3: "three"]
print(dict[0]) // Optional("zero")
print(dict.elements[0]) // (key: 2, value: "two")
关键点:
dict[0]查找 key 为0的 value。- 这个操作返回
Optional("zero")。 dict.elements是随机访问集合视图。dict.elements[0]返回位置 0 的键值对。- 演讲说明,
OrderedDictionary本身只符合Sequence,需要集合行为时使用elements。
核心启发
-
做什么:给聊天 App 增加“按时间间隔插入日期分隔条”。
- 为什么值得做:Swift Algorithms 的
chunked和joined已经覆盖“按相邻元素关系分组,再在组之间插入分隔项”的流程。 - 怎么开始:让消息 item 暴露
date,用.chunked { $1.date.timeIntervalSince($0.date) < 60 * 60 }分组,再用.joined { DateItem(date: $1.first!.date) }插入时间戳。
- 为什么值得做:Swift Algorithms 的
-
做什么:实现一个最近浏览照片栏,只展示最近 6 张照片。
- 为什么值得做:标准库算法链可以替代手写反向循环,
.lazy可以减少中间数组。 - 怎么开始:从 transcript 里
.lazy.compactMap { $0 as? PhotoItem }.suffix(6).reversed(),最后用Array(...)保存结果。
- 为什么值得做:标准库算法链可以替代手写反向循环,
-
做什么:给下载器或任务系统换成双端任务队列。
- 为什么值得做:
Deque对头尾插入和删除更合适,开头插入不用移动已有元素。 - 怎么开始:用
Deque<Task>保存待处理任务;普通任务append,高优先级任务prepend,消费任务时removeFirst()。
- 为什么值得做:
-
做什么:做一个可拖拽排序的标签管理器。
- 为什么值得做:
OrderedSet同时表达唯一性和用户指定顺序,适合“标签不能重复,但顺序要保存”的 UI。 - 怎么开始:用
OrderedSet<Tag>作为数据源;新增标签用append;拖拽后调用排序或移动逻辑;只比较成员时使用.unordered。
- 为什么值得做:
-
做什么:构建一个顺序稳定的配置编辑器。
- 为什么值得做:
OrderedDictionary可以按 key 快速查值,又能按插入顺序显示配置项。 - 怎么开始:用
OrderedDictionary<String, SettingValue>保存配置;读取值用dict[key];列表展示用dict.elements。
- 为什么值得做:
关联 Session
- What’s new in Swift — 介绍 Swift 5.5、并发模型和 Swift 开源包生态,能帮助理解这场 session 的语言背景。
- Swift concurrency: Update a sample app — 展示如何把新的 Swift 语言能力应用到真实 app 代码中。
- ARC in Swift: Basics and beyond — 解释 Swift 内存管理行为,和算法链、集合性能优化属于同一类基础能力。
- Write a DSL in Swift using result builders — 讲解 Swift 语法抽象能力,适合作为学习 Swift 表达力的延伸内容。
评论
GitHub Issues · utterances