Rust 劝退系列 05:复合数据类型
大家好,我是站长 polarisxu。
这是 Rust 劝退系列的第 5 个教程,探讨 Rust 中的复合数据类型(Compound types)。Rust 中有两种原生的复合类型:元组(tuple)和数组(array),顺带介绍切片。
01 元组类型
Go 语言没有元组类型,但多返回值有点类似元组(但还是有区别的哦)。Python 中有元组类型,因此如果你熟悉 Python,对元组应该很熟悉。
什么是元组类型?
元组是一个可以包含各种类型的值的组合。元组是一个将多个其他类型的值组合进一个复合类型的主要方式。元组长度固定:一旦声明,其长度无法增大或缩小。 元组的类型由各组成元素类型的序列定义。
元组通过小括号定义,里面的元素通过逗号分隔,例如:
(23.2, 27, 'a');
这个字面值元组的类型是:(f64, i32, char),即对应每个元素的默认类型。因此,我们可以通过 let 将这个元组绑定到变量上,Rust 会进行类型推断:
let tup = (23.2, 27, 'a');
在 VSCode 中可以看到 tup 的类型就是:(f64, i32, char)。同样地,我们也可以为 tup 使用类型注解:
let tup: (f32, i8, char) = (23.2, 27, 'a');
因为元组是多个类型的集合,对元组中的类型没有限制。因此,可以嵌套。比如:
(2, (2.1, 'a'), false);
不过建议别嵌套太多,否则可读性太差。
如何访问元组元素呢?
上面说,Go 语言中函数多返回值类似元组,在接收多返回值时,通过多个变量接收,比如:
// Go 语言
f, err := os.Open("abc.txt")
在 Rust 中,可以解构元组(这也叫模式匹配解构):
let tup = (23.2, 27, 'a');
let (x, y, z) = tup; // 注意:需要小括号
和 Go 语言一样,如果某个元素我们不关心,可以放入垃圾桶(_
):
let tup = (23.2, 27, 'a');
let (x, _, z) = tup; // 注意:需要小括号
Rust 中变量定义未使用,不会像 Go 一样报错,但会警告!
除了模式匹配解构,还可以使用类似访问数组元素的方式访问元组元素,只不过不是用[]
,而是用 .
加索引的方式(索引也是从 0 开始):
let tup = (23.2, 27, 'a');
println!("{}", tup.1); // 输出:27
特殊的元组
当元组中只有一个元素时(即元组长度是 1),唯一的元素后面必须加上逗号:
let tup = (2,); // 逗号不能少,否则会提示你,单个值应该去掉小括号。这是避免将小括号当做计算的优先级
自然,模式匹配解构元组时,也必须有逗号。
如果元组没有元素呢?即空元组。看下面的代码:
fn main() {
let result = test_tuple();
println!("{:?}", result);
}
fn test_tuple() {
println!("test empty tuple");
}
你猜打印 result 是啥?
擦,竟然是 ()
,即空元组。而且 Rust 给它专门取了一个名字:单元类型(unit type),也就是说,()
叫单元类型,它有一个唯一值:空元组 ()
。而且,因为没有任何元素,Rust 将其归为变量类型。
还嫌 Rust 不够复杂吗?就叫空元组不行吗?非得搞一个单元类型,这么奇怪的类型。。。
为了避免复杂性,我觉得大家将其理解为空元组即可。至于为什么这里会返回空元组,在函数部分会讲解。
注意:() 是不占空间的,这和 Go 中的空结构体类似。
02 数组
Rust 中的数组和 Go 中的类似,是不可变的,由元素类型和长度确定,且长度必须是编译期常量。Rust 中,数组类型标记为 [T; size]
。数组字面值使用 []
表示:
let a = [1, 2, 3, 4];
同样会进行类型推断(包括长度)(这里推断出 a 的类型是 [i32; 4]
),也可以显示进行类型注解:
let a: [i8; 4] = [1, 2, 3, 4];
相比较而言,Rust 创建数组比 Go 简单,它和 PHP 这样的动态语言类似。在 Go 中一般这样创建数组:
// Go 语言
a := [...]int{1, 2, 3, 4}
也就是说,Go 中创建数组是,类型信息不能少,没法跟 Rust 一样进行类型推断。
除了上面的初始化方法,Rust 中还可以这样简单的初始化:
let a = [-1; 4]; // 4 个元素都是 -1
Rust 变量必须初始化后才能使用,而 Go 语言中,变量会有默认值。所以,Go 中可以简单的定义一个数组,然后使用默认的初始值。如:
// Go 语言
var a [4]int // a 的值是:[0 0 0 0]
此外,Rust 中数组总是分配在栈中的,因此可以认为数组是「值类型」,和 Go 一样,我们不应该直接传递数组,而应该和 Go 一样,使用 slice。
03 切片(slice)
Rust 中的切片和 Go 中的切片意思一样,表示对数组部分元素的引用。但和 Go 不同的是,Rust 的切片没有容量的概念,只有一个指向数据的指针和切片的长度。Rust 中切片的类型标记为 &[T],即对数组进行引用(&)就是切片。
Go 语言中有直接创建切片的语法(比如 make),但 Rust 中没有,它必须依赖数组或 Vec(以后讲解),通过引用来创建。
let xs = [1, 2, 3, 4, 5];
let slice = &xs;
既然切片是数组元素的片段引用,那如何引用部分片段呢?
在 Go 中是这么做的:
var arr = [...]int{1, 2, 3, 4}
var slice1 = arr[:] // 结果是 [1 2 3 4],全部元素
var slice2 = arr[1:3] // 结果是 [2 3]
var slice3 = arr[:3] // 结果是 [1 2 3]
var slice4 = arr[1:] // 结果是 [2 3 4]
而在 Rust 中是这么做的:(结果和上面一样)
let arr = [1, 2, 3, 4];
let slice1 = &arr[..];
let slice2 = &arr[1..3];
let slice3 = &arr[..3];
let slice4 = &arr[1..];
看到不同了吗?
- Rust 中生成切片,需要引用(&);
- Go 中使用
:
来引用片段;而 Rust 使用..
;
相同的点是,都可以省略起始或终止位置,或都省略。
关于
..
以后还会讲到
切片类型的方法(也适用于数组)
在 Rust 中,一切类型都有实现一些 trait,包括上一节的标量类型(用面向对象来讲,一切皆对象)。现在先不探讨 trait,着重看看 len 方法。具体参考标准库文档:https://doc.rust-lang.org/std/primitive.slice.html。
1)len:计算长度
数组或切片有一个 len() 方法可以计算长度。
pub const fn len(&self) -> usize
// 具体使用
let arr = [1, 2, 3];
assert_eq!(arr.len(), 3); // assert_eq 和 println 一样,是一个宏,用来断言
而 Go 语言中,使用 len(arr) 的形式,len 是内置函数。
不过,关于 len 还有一些细小的点。看下面的 Go 代码,你觉得有问题吗?
var arr = [...]int{1, 2, 3, 4}
var slice = arr[:]
var arr2 [len(arr)]int
var arr3 [len(slice)]int
在 Go 中,要求数组长度要求是编译期常量。len(arr) 是编译期常量,而 len(slice) 却不是,因为 slice 的长度是可变的。所以,以上代码 arr2 正确,arr3 编译错误。
那 Rust 中是怎么样的呢?
let arr = [1, 2, 3, 4];
let slice = &arr[..];
let arr2 = [0;arr.len()];
let arr3 = [0;slice.len()];
arr2 和 arr3 都编译错误。arr3 错误可以理解,为什么 arr2 也不行呢?
根据编译器提示,怎么修改 arr2 就可以了:
const ARR:[i32; 4] = [1, 2, 3, 4];
let arr2 = [0; ARR.len()];
也就是说必须是数组常量。。。但数组本身不就是不可变的吗?非得定义成常量,多此一举?据说,Rust 有可能将数组改成可变的。。。有了切片,为啥还要把数组搞这么复杂?!
2)其他方法
- is_empty:判断数组或切片是否为空
- first:获取第一个元素
- last:获取最后一个元素
- 。。。
first 和 last 有什么用?为啥不直接通过下标获取?
- last 的存在,使得我们不需要先调用 len 获取长度来间接获取最后一个元素。
- 而 first 的存在,使得我们不需要先判断是否为空。
不过,因为存在数组或切片为空的情况,因此 first 和 last 返回的都是 Opiton 类型。关于该类型后续再讲。
04 小结
我们用两篇讲解了 Rust 中的数据类型,同时和 Go 的数据类型进行了对比。但 Rust 中的数据类型不止这些,还有其他类型,我们以后再讲,包括通过标准库定义的数据类型。
再强调一次,本系列教程的目标是让大家学习尽可能不被劝退,因此有些特别复杂但我认为可以不用的,就不会介绍。关于 Rust 中的 primitive type 可以在标准库文档找到,以及每个类型的方法。https://doc.rust-lang.org/std/index.html#primitives。