为什么Rust在运行时检查数组边界,而(大多数)其他检查都发生在编译时



阅读基本介绍:

如果您尝试使用不在数组中的下标,您将得到一个错误:数组访问是在运行时检查边界的。

为什么Rust在运行时检查数组边界,而大多数其他检查似乎都发生在编译时?

因为在一般情况下,在编译时检查索引是不可行的。即使对于小程序来说,对任意变量的可能值进行推理也介于困难和不可能之间。没有人愿意这样做:

  1. 正式证明索引不能越界,以及
  2. 将证明编码到类型系统中

对于每个切片/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在运行时而不是编译时处理数组索引越界错误的原因。

相关内容

最新更新