ex8: array 与 slice

数组(array)

数组(array)的概念基本上在所有的程序设计语言中都存在,其代表的含义是一个连续的地址空间,用于储存任意类型的数据。不过数组中数据的类型是否必须一致,数组的大小是否固定等问题,根据程序设计语言不同会有所区别。

Rust 对数组的要求很高:

  1. 数组中的元素类型必须相同

  2. 数组的大小必须在编译的时候确定

unsafe { 这导致了 Rust 不能像 C 和 C++ 那样动态地分配数组*,你需要在编译的时候就明确给出数组的长度。如果你不能做到这一点,那么你应该使用 Vector 来储存数据。(之后我们会提到) } *这句话从字面意义上理解可能是错误的:你当然可以动态地在堆上分配一个数组,但是就算如此你也必须在编译的时候明确给出数组的长度,而不能使用一个变量作为数组的长度。如果你确实有这个需要,那么你需要使用类似于vec![0; length]的语法来创建一个 Vector [1]

一般来说,为了深入理解数组的含义,你需要明白计算机中各个数据的储存位置以及储存方法。对于 C 和 C++ 的数组而言,其只需一个参数就可以描述它:数组的起始地址,之后你访问的任何内容都是通过这个起始地址和每一个元素的大小索引出来的——这很有可能导致数组越界,并且产生令人头疼的 Segment Fault。

而在 Rust 中,数组的长度同样也被储存下来,这可以使得数组被访问的时候不会越界(除非你偏要这么做)。在 Rust 中,数组的整体大小是确定的,所以它能储存在栈上。

切片(slice)

unsafe { 切片(slice)的概念在一些程序设计语言中也有出现*。你可以认为切片是数组的一部分,其具有两个属性:起始地址以及长度。从这个角度上来说,其储存的内容和数组引用相同的。不过其不实际储存任何数据,只是其他数据的一个引用。 }

*字符串也是可以进行切片的,也许可以认为字符串是一种字符数组。至于切片是否肯定不存储任何数据还需要再进行了解。

切片的长度是不固定的,也就是说它没办法在编译的时候确认一个切片的长度。Rust 称这种没办法在编译的时候得知长度的数据类型为 DST (dynamically sized type),Rust 规定 DST 不能出现在栈上(从某种角度上来说它们不能被直接绑定在变量上),所以比如 str(字符串类型)与 slice(切片)都需要通过“引用”的方法进行访问。

至于“引用”的具体含义可以参考 C 与 C++ 的指针,我们在之后会更加深入地讲解。目前可以先使用“指针”的概念进行理解。

代码

// #1
// 我们定义了另一个函数,你只需要注意切片的类型就可以
fn analyze_slice(slice: &[i32]) {
    println!("The whole slice: {:?}", slice);
    println!("The length of this slice: {}", slice.len());
}
// 如果有若干个函数,那么会从 main 函数开始运行
fn main() {
    // 数组:具有固定的大小,可以放在栈上
    let array: [i32; 5] = [0, 1, 2, 3, 4];
    let n_array: [i32; 10]; // 数组也可以稍后初始化,不过极不推荐
    n_array = [0; 10]; // 初始化为一个全是零的序列

    // 使用下标访问数组元素
    println!("array[0]: {}", array[0]); // 数组下标从 0 开始
    println!("array[4]: {}", array[4]); // 最后一个元素,下标是长度 - 1

    // `len`函数可以返回数组的长度
    println!("array size: {}", array.len());

    // 打印数组,使用{:?}
    println!("n_array: {:?}", n_array);

    // 切片:不知道有多大,不可以直接放在栈上
    // 下面这行不正确,我们替你注释掉了
    // let slice: [i32] = array[0..2]; // [i32]是一个切片的类型,因为没有长度所以不能放在栈上
    // 0..2代表一个区间,左闭右开 #2
    let slice: &[i32] = &array[0..2]; // &[i32]在栈上的是一个切片的引用,大小为两个 usize
    println!("{:?}", slice);

    // 数组整体上可以被认为是一个切片
    // 如果需要进行转化的话,数组可以自动被当作切片进行“借用”(borrow)
    // 下面这行是错误的,我们替你注释掉了
    // analyze_slice(array);
    analyze_slice(&array); // 如果想要进行“借用”,那么必须使用 & 符号。细节留到后面

    // 借用一部分数组作为切片
    analyze_slice(&array[0..=3]); // 0..=3代表一个区间,两边均是闭区间

    // 数组访问不允许越界
    let pos = 5; 
    // !error! 下面这句话不正确
    println!("array[5]: {}", array[pos]); // 长度为5的数组能访问的最大下标为4

    // 编译器有的时候可以帮你解决一些问题,不过你可以瞒过去
    for i in 0..6 { //#3
        // 编译器没办法找到这个地方的问题
        // 不过 Rust 已经想了办法阻止越界访问导致的结果
        println!("array[{}]: {}", i, array[i]);
    }
}

代码说明

#1

在这一次的代码中,我们又定义了一个全新的函数。这个函数和 main 差不多,它们是默认返回类型,只不过这个函数接受一个参数:slice。在 Rust 中,每一个参数的类型都需要进行标注,这也方便编译器判断你是否正确地使用了函数。

有关函数的相关内容可以查看后面的练习。

#2

在代码中看到的0..20..=3的写法规定了一个区间,它们也有自己的类型 Range。这是一个标准库类型,具有很多功能。我们现在只要知道它可以用来表示区间,并且用来分隔数组就可以了。

#3

这里的语句的含义是使用下标 0 到 5 分别访问数组元素并且输出。语法上的原理可以并不明白,但是功能上是这样。具体的细节我们之后会讲到。

本节总结

在本节中,我们主要说明了复合数据类型 array 以及 slice。从这一节开始我们可能会无法避免地接触到 Rust 语言的一些较为高级的特性,如果感觉难以理解,可以暂时忽略它们。

你应该对下面的内容有所掌握:

  1. array 的创建、初始化、元素访问

  2. array 的大小是固定的,访问不能出界

你应该对下面的内容有所了解:

  1. array 可以放在栈上,slice 不行

  2. array 和 slice 的类型标注

  3. 如果想要利用 slice ,则需要使用&进行借用

  4. for .. in ..循环有一个印象

  5. 对 main 函数以外的函数定义有一个印象

碎碎念

这一节中出现了一些很难理解的概念,比如说“引用”、“借用”。如果你有其他语言的基础的话,可以先利用其他语言的方法来理解它们。Rust 在这些方面确实非常神奇,但是于此同时理解难度也会加大。如果感觉理解起来有一些困难,不妨先把“理解”这件事先放下,往后看一看,说不定什么时候就会明白了。

注:如果你看 Rust By Example 可能也会有这样的感觉,很多之前的章节依赖了之后的内容。主要是因为 Rust 中这些基本的概念实在是联系太紧密了,很难把它们都分开。

参考资料

最后更新于