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

12

阅读基本介绍

如果您尝试使用不在数组中的下标,则会出现错误:数组访问在运行时进行边界检查。

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


可能是为什么Rust编译器允许索引越界?的重复问题。 - Shepmaster
@Shepmaster,那个问题是关于向量的,而这个问题是关于数组的。 - mherzl
2个回答

19

由于在一般情况下,在编译时检查索引是不可行的。即使对于小程序,推理任意变量的可能值也在困难和不可能之间。没有人想要:

  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];

3
认为运行时边界检查是“非常不具侵入性”的说法有些牵强。其影响将与正在处理数组的算法复杂度相关。对于像运行时间这样的度量,基本上在每个数组访问上添加一个边界检查是一个恒定的乘数。 - Rob
@Rob 某些操作的运行时间中的常数因子不会改变算法复杂度。它可能会对(非渐近)运行时间产生不可接受的影响。但正如我所说,对于任何单个数组访问,程序员都可以选择退出边界检查,如果他们这样做,性能等同于 C。边界检查不会影响任何不使用它的程序部分。这就是我所说的非侵入式(我在同一句话中提到了性能影响)。 - user395760
1
我并没有暗示算法复杂度会有任何改变。能够选择退出运行时特性并不意味着它是无侵入性的。 - Rob
1
回答你的问题,一个侵入式运行时特性是指在现实场景下具有广泛影响并且可能在运行时产生显著影响的特性,除非明确禁用(或未使用)。也就是说,这是对英语医学定义的不完美适应(侵入性程序具有广泛和/或强烈的局部效应)。非侵入式特性则相反。顺便说一句,我并不是在暗示运行时边界检查是好还是坏,只是觉得“非侵入式”这个术语似乎承诺过多。 - Rob
2
Rust知道数组的长度,但在编译时可能不总是知道索引值,因为它可以是从函数调用返回的变量。元组则不同,元组的索引实际上是字段名(类似于结构体的字段名),不能是变量。 - Xiao-Feng Li
显示剩余4条评论

0
原因是: 尽管数组的长度可能事先已知,但要引用的索引可能事先不知道。
简单的例子:
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在运行时而不是编译时处理数组索引越界错误的原因。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接