阅读基本介绍:
如果您尝试使用不在数组中的下标,您将得到一个错误:数组访问是在运行时检查边界的。
为什么Rust在运行时检查数组边界,而大多数其他检查似乎都发生在编译时?
因为在一般情况下,在编译时检查索引是不可行的。即使对于小程序来说,对任意变量的可能值进行推理也介于困难和不可能之间。没有人愿意这样做:
- 正式证明索引不能越界,以及
- 将证明编码到类型系统中
对于每个切片/Vec
/等。访问,因为这是在编译时执行边界检查所必须做的。您基本上需要依赖类型。
除了可能使类型检查变得不可判定(并使程序更难进行类型检查)之外,类型推理在一般情况下变得不可能(在最好的情况下受到更大的限制),类型变得更加复杂和冗长,语言的复杂性显著增加。只有在非常简单的情况下,在没有大量额外程序员工作的情况下才能证明索引是有界的。
此外,几乎没有动力取消边界检查。生命周期几乎完全消除了垃圾收集的需求,这是一个巨大的侵入性功能,具有不可预测的吞吐量、空间和延迟影响。另一方面,运行时边界检查是非常非侵入性的,开销很小,而且在性能关键的部分可以选择性地关闭,即使整个程序的其余部分都大量使用它。
请注意,编译器可以对数组的越界访问进行一些简单的检查:
let a = [1, 2];
let element = a[100];
error: index out of bounds: the len is 2 but the index is 100
--> src/main.rs:3:19
|
3 | let element = a[100];
| ^^^^^^
|
= note: #[deny(const_err)] on by default
然而,这是有限的,通过使指数值不是一个"明显"的常数很容易避免:
let a = [1, 2];
let idx = 100;
let element = a[idx];
原因是:尽管可以提前知道数组的长度,但被引用的索引可能不会提前知道。
简单示例:
fn main() {
let a = [1,2,3,4,5];
let index = 10;
let element = a[index];
}
在rust中,数组是堆栈变量,因此它们的长度不会改变。因此,数组的长度在编译时是已知的。
在看到上面的例子之后,rust似乎应该能够在编译时检查"索引越界"错误。
然而,尽管数组大小在编译时是已知的,但引用数组的索引在编译时通常是未知的。
考虑这个更复杂的例子:
fn main() {
let a = [1,2,3,4,5];
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
let element = a[guess];
}
访问数组的索引来自用户输入。编译器无法知道访问数组的索引。要知道这里的索引,唯一的方法就是运行程序。这就是为什么rust在运行时而不是编译时处理数组索引越界错误的原因。