批量上传记录、拆分数据库查询条件或分批刷新列表时,Swift 项目常有一个“每次取若干元素”的辅助函数。它在完整数组上运行正常,换成 dropFirst 返回的切片,却可能越界或漏掉尾部。把这类纯逻辑放到云端 Mac 的构建前检查,不必启动模拟器,也能提前拦住批处理回归。
先固定分块契约
本次只验收连续分块:每块最多包含指定数量的元素,保持输入顺序,最后一块允许不足,不补齐、不丢弃。空集合返回空结果,块长小于等于零则抛出错误。
例如,五个元素按两个一组,应得到长度为二、二、一的三块,而不是只返回两个完整块。块长大于输入长度时,整个非空输入就是唯一一块。
分块函数负责划定元素边界,不负责限制上传并发,也不代表远端接口已经接收成功。批次完成状态需要由调用层单独记录。
测试应检查展平后的结果与输入完全一致。只检查块数,无法发现中间重复一个元素、同时遗漏另一个元素的问题。
用集合索引推进边界
ArraySlice 保留原数组的索引。对十个元素执行 dropFirst(5),得到的切片起点不是零。泛型 Collection 的索引甚至不一定是整数,因此不能用 0..<count 直接下标访问。
下面的实现逐个推进索引,既不计算“起点加块长”,也不依赖随机访问能力。将它保存为 ChunkChecks.swift:
enum ChunkError: Error {
case invalidSize
}
func chunks<C: Collection>(
_ input: C,
size: Int
) throws -> [C.SubSequence] {
guard size > 0 else {
throw ChunkError.invalidSize
}
var result: [C.SubSequence] = []
var start = input.startIndex
while start != input.endIndex {
var end = start
var taken = 0
while end != input.endIndex && taken < size {
input.formIndex(after: &end)
taken += 1
}
result.append(input[start..<end])
start = end
}
return result
}
每次索引推进前都确认尚未到达 endIndex,最终用半开区间截取。即使块长是 Int.max,也只走过实际存在的元素,不尝试生成超过集合边界的整数索引。
这里没有反复调用剩余集合的 distance。对非随机访问集合,反复测量剩余长度可能产生不必要的重复遍历;当前实现的索引推进次数与输入元素数相同。
覆盖切片与尾块组合
继续在同一文件末尾加入测试。先固定一个可以人工核对的例子,再遍历多种输入长度和块长,避免只证明某个特例可用。
let base = Array(0..<23)
let slice = base.dropFirst(5).prefix(5)
precondition(slice.startIndex == 5)
let example = try chunks(slice, size: 2)
precondition(example.map { Array($0) } == [
[5, 6], [7, 8], [9]
])
for length in 0...17 {
let input = base.dropFirst(5).prefix(length)
for size in 1...8 {
let output = try chunks(input, size: size)
let expectedCount =
length / size + (length % size == 0 ? 0 : 1)
precondition(output.count == expectedCount)
precondition(output.flatMap { Array($0) } == Array(input))
for (position, block) in output.enumerated() {
precondition(!block.isEmpty)
precondition(block.count <= size)
if position < output.count - 1 {
precondition(block.count == size)
}
}
}
}
let huge = try chunks(slice, size: Int.max)
precondition(huge.map { Array($0) } == [Array(slice)])
for size in [0, -1, Int.min] {
do {
_ = try chunks(slice, size: size)
preconditionFailure("Expected invalidSize")
} catch ChunkError.invalidSize {
}
}
预期块数用商和余数计算,而不是常见的“长度加块长减一再相除”。后一种公式在极大参数下可能先发生整数溢出。
测试同时要求所有非末块填满、末块非空、展平结果不变。非法参数只接受指定错误;其他意外错误会直接让脚本失败,不能用宽泛捕获把故障吞掉。
接入云端 Mac 的构建前检查
在已配置 Swift 工具链的云端 Mac 上,从仓库根目录运行:
set -eu
xcrun swift --version
xcrun swift Scripts/ChunkChecks.swift
把文件放入仓库的 Scripts 目录,并将这一步放在耗时的应用编译之前。默认执行模式下,任一前置条件失败都会产生非零退出状态;脚本没有输出不代表没有执行,应以任务状态和退出码验收。
不要使用会移除安全检查的 -Ounchecked 运行这些断言。团队若把逻辑移入测试目标,应将前置条件改为测试断言,使失败信息能指出具体输入长度、块长和批次位置。
独立脚本适合建立最小复现。正式接入后,应调用项目实际使用的实现,避免测试文件复制了一份正确代码,而业务代码仍保留旧算法。
划清存储与业务边界
返回切片能减少立即复制,但不等于释放原输入。即使只留下最后一小块,切片仍可能保留整个数组的底层存储。长期保存少量批次时,可用 blocks.map { Array($0) } 转成独立数组;若所有批次都保留,元素总量并不会因此减少。
超大输入也不适合一次收集所有切片。可以把同一套边界推进逻辑改成逐批回调或惰性迭代,让调用方处理完一块再请求下一块,并明确背压与失败重试策略。
最后保留三个检查项:非零索引输入通过、尾块没有遗漏、批次展平后顺序不变。分块验收先保证“哪些元素属于哪一批”,网络重试和并发调度再分别测试,故障才能定位到明确层次。
常见问题
ArraySlice 的第一个元素可以用索引零访问吗?
不能假定可以。ArraySlice 保留原集合索引,应使用 startIndex、endIndex 和集合索引方法;需要从零开始的独立数组时,显式转换为 Array。
分块后保存少量切片会释放原数组吗?
不保证。切片可能继续保留原数组的底层存储;需要长时间保存少量结果时,应转换为独立数组,并同时控制累计结果的大小。
让整台 Mac 只跑团队的构建
选择 Apple Silicon M4 独享物理 Mac mini,保留团队自己的工具链与 runner 配置。按天、周、月、季计费,可用性以控制台实时返回为准。
租用构建节点