下载模块按“分块数 × 块大小”预留容量,图片处理按“行数 × 每行字节数”申请缓冲区。这些运算平时没有异常,遇到损坏文件或异常接口数据,却可能先触发整数溢出,再让应用崩溃。在云端 Mac 上,可以把纯算术逻辑抽成一个 Swift 可执行检查,不启动模拟器,也能为 iOS 工程建立稳定的边界回归。
先把容量计算定义成契约
本文只处理非负整数容量,不涉及浮点近似值。约定三条规则:负数属于非法输入;超出 Int 范围返回溢出错误;向上取整的除数必须大于零。零容量合法,但零除数不合法。
不要先转换成无符号类型再校验。负数到无符号数的普通转换可能直接陷阱退出;截断转换又可能把它变成巨大的正数。输入来自字符串时,也应先确认 Int 解析成功,不能把解析失败默认成零。
算术结果可表示,不代表容量可以分配。整数范围检查和业务配额检查是两道独立门禁,二者都通过后才允许申请资源。
把“数量”“单项字节数”“总字节数”分别命名,避免把不同单位混用。错误还应携带运算阶段,但不要把原始敏感输入整段写进日志。
用报告式运算代替回绕
普通 +、* 在溢出时会触发运行时错误;&+、&* 则明确执行回绕,可能把超大容量变成小值。容量校验需要的是报告式运算:同时拿到结果与溢出标记,先检查标记,再使用结果。
将下面代码保存为 arithmetic.swift。它没有依赖应用工程,也不执行真实的大内存分配。
enum CapacityError: Error, Equatable {
case invalidInput
case overflow
}
func checkedAdd(_ a: Int, _ b: Int) throws -> Int {
guard a >= 0, b >= 0 else {
throw CapacityError.invalidInput
}
let result = a.addingReportingOverflow(b)
guard !result.overflow else {
throw CapacityError.overflow
}
return result.partialValue
}
func checkedMultiply(_ a: Int, _ b: Int) throws -> Int {
guard a >= 0, b >= 0 else {
throw CapacityError.invalidInput
}
let result = a.multipliedReportingOverflow(by: b)
guard !result.overflow else {
throw CapacityError.overflow
}
return result.partialValue
}
func ceilingDivide(_ n: Int, by d: Int) throws -> Int {
guard n >= 0, d > 0 else {
throw CapacityError.invalidInput
}
let q = n / d
return n % d == 0 ? q : try checkedAdd(q, 1)
}
func require(_ condition: Bool) {
guard condition else {
fatalError("Boundary check failed")
}
}
func expectError(
_ expected: CapacityError,
_ operation: () throws -> Int
) {
do {
_ = try operation()
fatalError("Expected capacity error")
} catch let error as CapacityError {
require(error == expected)
} catch {
fatalError("Unexpected error type")
}
}
require(try checkedAdd(Int.max, 0) == Int.max)
require(try checkedMultiply(Int.max, 1) == Int.max)
require(try checkedMultiply(0, Int.max) == 0)
require(try ceilingDivide(0, by: 4) == 0)
require(try ceilingDivide(8, by: 4) == 2)
require(try ceilingDivide(9, by: 4) == 3)
require(try ceilingDivide(Int.max, by: 1) == Int.max)
require(try ceilingDivide(Int.max, by: 2) == Int.max / 2 + 1)
expectError(.overflow) {
try checkedAdd(Int.max, 1)
}
expectError(.overflow) {
try checkedMultiply(Int.max, 2)
}
expectError(.invalidInput) {
try checkedMultiply(-1, 0)
}
expectError(.invalidInput) {
try ceilingDivide(1, by: 0)
}
expectError(.invalidInput) {
try ceilingDivide(Int.min, by: -1)
}
这里故意用负数乘零测试校验顺序。若实现先做“乘数为零就返回零”的快捷处理,非法输入就会绕过契约。测试必须检查错误类型,不能只确认“发生过某个错误”。
向上取整不要先加除数
分块数量常写成 (n + d - 1) / d。数学上成立,不代表固定宽度整数上安全:即使最终商很小,前面的加法也可能越界。
示例先求商和余数,有余数才加一。输入限定为非负数、正除数后,除法也不会碰到有符号整数最小值除以负一的特殊溢出边界。
测试至少覆盖零、整除、余一、最大整数和零除数。不要将 Int.max 写死成某个十进制常量;直接使用类型提供的边界,检查的是实际编译目标的整数宽度,而不是开发者记忆中的平台假设。
在两种编译模式下执行
云端 Mac 需要已选中的开发工具链。先确认编译器可用,再分别编译运行:
set -eu
xcrun swiftc --version
mkdir -p .arithmetic-check
xcrun swiftc -Onone arithmetic.swift \
-o .arithmetic-check/debug
.arithmetic-check/debug
xcrun swiftc -O arithmetic.swift \
-o .arithmetic-check/optimized
.arithmetic-check/optimized
成功时程序静默退出;任何断言失败或未处理错误都会使步骤失败。不要用管道把退出码变成日志工具的成功状态。
示例没有使用 assert,因为优化构建可能移除它。自定义的 require 使用显式条件和 fatalError,在这里采用的两种模式下都保留检查。也不要额外启用会削弱运行时安全检查的非标准优化设置。
接入应用时再检查资源上限
这份独立程序是算术契约的最小验证。正式接入时,应让应用与测试引用同一份生产实现,而不是长期维护两份相似函数。
按以下顺序检查调用链:
- 原始输入解析失败立即返回错误,不使用默认容量继续执行。
- 各项单位一致后再进行受检乘加,避免中间表达式先溢出。
- 将结果与产品定义的单次配额比较,再决定是否分配。
- 转换到更窄整数类型时使用可失败的精确转换,拒绝截断。
- 验收失败路径没有创建文件、申请缓冲区或提交后续任务。
最后增加一条“算术合法但超过业务配额”的样例,确认它与溢出错误分开处理。这样定位失败时,就能明确区分输入不合法、类型容纳不下和资源策略拒绝,而不是把所有边界问题都归为内存不足。
常见问题
容量计算溢出后可以直接返回 Int.max 吗?
不建议默认这样做。饱和值会混淆合法的大容量与失败结果,还可能导致后续分配异常。应返回明确错误;只有业务契约明确要求饱和计算时才采用饱和值。
这套检查需要启动 iOS 模拟器吗?
示例只使用 Swift 标准库,可在云端 Mac 上直接编译运行。它验证算术契约;涉及应用界面、平台类型或真实内存分配的行为仍需单独做平台测试。
把构建任务交给独享云端 Mac
选择 Apple Silicon 物理节点,独占整台机器,使用原生 macOS 图形界面与命令行。机型、周期和节点以订单确认内容为准。