WWDC Quick Look 💓 By SwiftGGTeam
Meet the Swift Algorithms and Collections packages

Meet the Swift Algorithms and Collections packages

观看原视频

Highlight

Apple 发布 Swift Algorithms 和 Swift Collections 两个开源包,让开发者现在就能使用新的序列算法、双端队列、有序集合和有序字典,并把这些 API 作为未来标准库能力的孵化场。

核心内容

一个聊天 App 经常有这种代码:用户选中几行消息,你要拿到对应的 Message;用户点开图片,你要从消息里收集附件;详情页要显示最近 6 张照片。最直接的写法是 for 循环、临时数组、if letbreak

这些循环能工作,但它们把意图藏在控制流里。读代码的人需要看完整个循环,才能知道它是在转换、过滤、展平,还是只取前几个元素。循环还容易漏掉容量预留、延迟计算这些性能细节。

Swift 标准库已经有 mapcompactMapflatMapprefixsuffixreversed 这些算法。Swift Algorithms 继续补齐标准库还没有覆盖的模式,例如滑动窗口、相邻元素、分块、按条件分组。

Apple 同时发布 Swift Collections。Swift 标准库长期只有三类通用数据结构:ArraySetDictionary。它们适合做通用边界类型,但队列、有序唯一列表、有序键值表这些场景,需要更贴近问题的数据结构。

Swift Collections 提供 DequeOrderedSetOrderedDictionary。它们保持 Swift 集合的值语义和 copy-on-write 行为,同时补上标准库缺少的操作模型。

从原始循环走向算法词汇

聊天 App 的代码里,很多循环其实都有名字。选中行转消息,是 map。收集非空附件,是 compactMap。一条消息生成多个 transcript item 后再展平,是 flatMap

有了这些名字,代码的重心从“怎么循环”变成“这一步在做什么”。这也是 Swift Algorithms 的定位:把缺失的常见模式先放进开源包,开发者现在使用,社区再一起验证它们是否适合进入标准库。

从通用集合走向合适的数据结构

Array 很适合按下标访问,但在开头插入元素需要移动已有元素。队列、任务调度、聊天草稿撤销栈这类场景,经常从头尾两端插入和移除,Array 的成本偏高。

Set 保证唯一性,但不保证顺序。待办列表、标签列表、用户手动排序的收藏夹,需要同时满足“元素唯一”和“顺序稳定”。这就是 OrderedSet 的位置。

Dictionary 可以按 key 快速查值,但键值对顺序没有作为核心语义暴露。需要按插入顺序展示字段、参数、配置项时,OrderedDictionary 把 key 查找和顺序保存放在同一个类型里。

详细内容

标准库算法:把循环改写成可读的操作

01:00map 用来把输入集合里的每个元素转换成另一个值。演讲中的例子来自表格选中行: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:36compactMap 处理“过滤 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:06flatMap 处理“一对多转换”。一条消息可能生成多个 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,可以让 mapfiltercompactMap 这类操作延迟执行。

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:45chunks(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:49chunked 也可以用自定义条件分组。演讲用它实现聊天记录的时间戳插入:相邻 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:56Deque 是 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:46Deque 的 API 接近 Array。它符合 RandomAccessCollectionMutableCollectionRangeReplaceableCollection,可以按整数下标访问、修改、插入范围。

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:39Deque 在中间删除范围时,可以选择移动前半段或后半段元素来填补空洞。演讲给出的结论是,随机位置删除平均快两倍。

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:26OrderedSet 保存插入顺序。顺序参与相等性比较;如果只关心成员集合,可以使用 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:04OrderedSet 像数组一样支持整数下标、排序和洗牌,也像集合一样避免重复元素。

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:46OrderedDictionary 是有序版字典。它用 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:38OrderedDictionary 的下标永远表示 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

核心启发

  1. 做什么:给聊天 App 增加“按时间间隔插入日期分隔条”。

    • 为什么值得做:Swift Algorithms 的 chunkedjoined 已经覆盖“按相邻元素关系分组,再在组之间插入分隔项”的流程。
    • 怎么开始:让消息 item 暴露 date,用 .chunked { $1.date.timeIntervalSince($0.date) < 60 * 60 } 分组,再用 .joined { DateItem(date: $1.first!.date) } 插入时间戳。
  2. 做什么:实现一个最近浏览照片栏,只展示最近 6 张照片。

    • 为什么值得做:标准库算法链可以替代手写反向循环,.lazy 可以减少中间数组。
    • 怎么开始:从 transcript 里 .lazy.compactMap { $0 as? PhotoItem }.suffix(6).reversed(),最后用 Array(...) 保存结果。
  3. 做什么:给下载器或任务系统换成双端任务队列。

    • 为什么值得做Deque 对头尾插入和删除更合适,开头插入不用移动已有元素。
    • 怎么开始:用 Deque<Task> 保存待处理任务;普通任务 append,高优先级任务 prepend,消费任务时 removeFirst()
  4. 做什么:做一个可拖拽排序的标签管理器。

    • 为什么值得做OrderedSet 同时表达唯一性和用户指定顺序,适合“标签不能重复,但顺序要保存”的 UI。
    • 怎么开始:用 OrderedSet<Tag> 作为数据源;新增标签用 append;拖拽后调用排序或移动逻辑;只比较成员时使用 .unordered
  5. 做什么:构建一个顺序稳定的配置编辑器。

    • 为什么值得做OrderedDictionary 可以按 key 快速查值,又能按插入顺序显示配置项。
    • 怎么开始:用 OrderedDictionary<String, SettingValue> 保存配置;读取值用 dict[key];列表展示用 dict.elements

关联 Session

评论

GitHub Issues · utterances