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