溢出
一个数的阶乘增长速度相当快。例如,20的阶乘是2,432,902,008,176,640,000,这已经超过了32位整数的最大值2,147,483,647。
当算术操作的结果超过给定整数类型的极限值时,我们就遇到了整数溢出的问题。
整数溢出是个问题,因为它违背了算术操作的约定。两个特定类型整数之间的算术操作结果应该还是同类型的另一个整数。但数学上正确的结果却超出了那个整数类型能表示的范围!
如果结果小于给定整数类型的最小值,我们称其为整数下溢。
为了简洁,本节余下的部分我们将只讨论整数溢出,但请记住,我们所说的一切同样适用于整数下溢。你在"变量"部分编写的
speed
函数在某些输入组合下发生了下溢。
例如,如果end
小于start
,那么end - start
将会导致u32
类型下溢,因为结果应该是负数,而u32
无法表示负数。
无自动提升
一种可能的处理方式是自动将结果提升到更大的整数类型。
例如,当你相加两个u8
整数,如果结果是256(u8::MAX + 1
),Rust可以选择将结果解释为u16
,这是足够容纳256的下一个整数类型。
然而,正如我们之前讨论的,Rust对类型转换相当严格。自动整数提升并不是Rust解决整数溢出问题的方法。
替代方案
既然排除了自动提升,当发生整数溢出时我们能做什么呢?
归结起来有两种不同的方法:
- 拒绝该操作
- 提出一个“合理”的结果,使其适应预期的整数类型
拒绝操作
这是最保守的方法:当发生整数溢出时停止程序。
这是通过我们在"恐慌"部分已经见过的恐慌机制来实现的。
提出一个“合理”的结果
当算术操作的结果超过给定整数类型的最大值时,你可以选择环绕。
如果你将给定整数类型的所有可能值想象成一个圆圈,环绕就意味着当你达到最大值时,你又从最小值开始。
例如,如果你在1和255(=u8::MAX
)之间进行环绕加法,结果是0(=u8::MIN
)。如果你使用有符号整数,同样的原则适用。例如,将1加到127(=i8::MAX
)并采用环绕,将给你-128(=i8::MIN
)。
overflow-checks
Rust让你,作为开发者,选择当整数溢出时采取哪种方法。
这种行为由overflow-checks
配置设置控制。
如果overflow-checks
设置为true
,Rust在整数操作溢出时将在运行时恐慌。如果overflow-checks
设置为false
,Rust在整数操作溢出时将环绕。
你可能想知道——配置文件设置是什么?让我们来了解一下!
配置文件
一个配置文件是一组配置选项,可以用来定制Rust代码的编译方式。
Cargo提供了两个内置的配置文件:dev
和release
。
dev
配置文件在每次你运行cargo build
、cargo run
或cargo test
时使用。它旨在本地开发,因此牺牲了运行时性能以换取更快的编译时间和更好的调试体验。
相反,release
配置文件针对运行时性能进行了优化,但会导致更长的编译时间。你需要通过--release
标志明确请求——例如,cargo build --release
或cargo run --release
。
“你是否以发布模式构建了你的项目?”几乎成了Rust社区的一个梗。
它指的是不熟悉Rust并在社交媒体(如Reddit、Twitter等)上抱怨其性能,却没意识到自己还没以发布模式构建项目的开发者。
你还可以定义自定义配置文件或自定义内置文件。
overflow-check
设置
默认情况下,overflow-checks
设置为:
- 对于
dev
配置文件设为true
- 对于
release
配置文件设为false
这符合两个配置文件的目标。
dev
旨在本地开发,因此它会在尽可能早的时候出现潜在问题时恐慌。
而release
则是为运行时性能优化:检查溢出会减慢程序,所以它更倾向于环绕。
同时,两个配置文件的不同行为可能导致微妙的bug。
我们的建议是对两个配置文件都启用overflow-checks
:宁可崩溃也不要默默地产生错误结果。在大多数情况下,运行时性能的影响微乎其微;如果你正在开发对性能要求严格的应用程序,你可以运行基准测试来决定是否能够接受这一点。
参考资料
- 本节练习位于
exercises/02_basic_calculator/08_overflow
进一步阅读
- 查看"关于Rust中整数溢出的神话与传说"深入了解Rust中的整数溢出。
你可以尝试捕捉恐慌,但这应该是仅在非常特殊的情况下才考虑的最后手段。