携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第29天,点击查看活动详情
今天我们接着昨天的 标量 4 种数据类型继续 学习 Rust 提供的原生的复合类型(Compound) 。
Rust 提供的复合类型一共有两种: tuple(元组),array(数组)。
Tuple
Rust 的 tuple 可以将一系列不同类型的值组装到一起,变成一个复合类型的值。有过 Python 开发经验的同学对 tuple 会比较熟悉。
tuple 的长度是固定的,一旦声明,就不能再扩容/缩容。声明一个 tuple 很简单:
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
我们通过 (500, 6.4, 1) 这样的逗号分隔格式就可以创建一个 tuple。注意到我们这里有三个元素,每个元素都有自己的类型,它们不必非得一样。这里我们还加上了完整的类型声明,如果不加编译器会自动推断。
事实上这样也是可以的:
let tup = (500, 6.4, 1);
上面我们声明的 tup 变量是和整个 tuple 绑定的,它们是一个整体。我们当然也可以获取 tuple 中的值,这里有两种做法:
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {y}");
}
上面这种是直接赋值,变量长度和 tup 长度匹配即可。
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
}
第二种方法则是用 . + index 来直接根据序号获取,上面这个例子里,想获取 x 这个 tuple 的第一个值,我们就可以用 x.0 (index 都是从 0 开始的,这个大家很熟悉)。
需要特别提一下的是,我们也可以声明一个完全不包含任何值的 tuple,这类 tuple 有个专用的名字:unit。类似这样:let _u = ();。目前大家记住有这个概念即可,后面的章节我们会看到它是怎么用到的。
array
array 是一个定长的数组,所有元素的类型必须一致,这也是跟 tuple 最大的区别。
现代高级语言一般都会提供两个类型给开发者选择,一个是定长的数组,一个是可扩容/缩容的数组,后者效率不如前者高,但覆盖场景更多。比如在 Golang 中的 array 和 slice。
在 Rust 中,这个可扩容/缩容的数组叫做 vector,同样是由标准库提供的。我们会在之后的章节介绍。
array 这种定长的数组的效率更高,如果你知道这个数组内的元素数量不会变,用 array 就是最佳选择,例如存储一年的月份,可以这样:
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
当然,在需要类型的时候,我们可以用下面的形式来声明:
let a: [i32; 5] = [1, 2, 3, 4, 5];
方括号里分号前面的部分是类型 i32,后一部分指的是我们的 array 包含 5 个元素。
还有些时候我们希望声明一个所有元素的值都相同的数组,可以这样:
let a = [3; 5];
一定要分清这两个,前面那个 [i32; 5] 是在类型的部分,这里的 [3; 5] 是在值的部分,意味着我希望申请一个 5 个元素长度的数组,所有值都是 3。
等价于这样的写法:
let a = [3, 3, 3, 3, 3];
访问数组元素
array 还有一个很重要的属性:所有的数据都在 栈(stack)上,它是一段连续的,固定长度的内存。
我们访问 array 中某个元素的方式和 tuple 略有区别。
- tuple
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
- array
let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
array 中我们用方括号 + 下标来访问元素。
非法访问
如果 Rust 中你访问了超出数组长度的下标,会怎么样?我们将 guess number 中的示例稍微改一下(代码应该很好理解了,如果不熟悉的同学建议回看一下咱们这个系列的 2,3 两篇文章)
use std::io;
fn main() {
let a = [1, 2, 3, 4, 5];
println!("Please enter an array index.");
let mut index = String::new();
io::stdin()
.read_line(&mut index)
.expect("Failed to read line");
let index: usize = index
.trim()
.parse()
.expect("Index entered was not a number");
let element = a[index];
println!("The value of the element at index {index} is: {element}");
}
这段代码本身有没有语法问题?
没有,编译是能过的,说白了问题在于 index 是从外部输入的,当你输入 0,1,2,3,4 这几个小于等于数组长度的下标时,没问题,能取到数据。
但是,如果你输入了个 10 呢?
$ cargo run
================================
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/compound`
Please enter an array index.
10
thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:33:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
出现了 runtime error,程序还是 panic 了,提示数组下标越界,后面的 println 没有执行,整体程序就退出了。
这一点也是符合 Rust 对于内存安全的原则,如果不检查下标,允许你访问了,就会访问到别的变量数据,这一定是不符合预期的。
基本数据类型示例
到这里,我们就把 4 种标量(整型,浮点型,布尔,字符),2种复合类型(元组,数组)学完了。其实 Rust 提供的数据类型还很多。这里我们学到的 6 种是最基础的 Primitives。在此之上还可以封装出来各种各样的数据结构。
文章的最后,我们参考 Rust 的 example 看一个例子,把我们这两篇文章的精华用代码注释来串一下:
fn main() {
// Variables can be type annotated.
let logical: bool = true;
let a_float: f64 = 1.0; // Regular annotation
let an_integer = 5i32; // Suffix annotation
// Or a default will be used.
let default_float = 3.0; // `f64`
let default_integer = 7; // `i32`
// A type can also be inferred from context
let mut inferred_type = 12; // Type i64 is inferred from another line
inferred_type = 4294967296i64;
// A mutable variable's value can be changed.
let mut mutable = 12; // Mutable `i32`
mutable = 21;
// Error! The type of a variable can't be changed.
mutable = true;
// Variables can be overwritten with shadowing.
let mutable = true;
}
感谢阅读!欢迎评论区交流。