Rust编程:入门、实战与进阶
上QQ阅读APP看书,第一时间看更新

2.3 复合数据类型

复合数据类型是由其他类型组合而成的类型。Rust的复合数据类型有元组、数组、结构体、枚举等。

2.3.1 元组类型

元组类型是由一个或多个类型的元素组合成的复合类型,使用小括号“()”把所有元素放在一起。元素之间使用逗号“,”分隔。元组中的每个元素都有各自的类型,且这些元素的类型可以不同。元组的长度固定,一旦定义就不能再增长或缩短。如果显式指定了元组的数据类型,那么元素的个数必须和数据类型的个数相同。

我们可以使用“元组名.索引”来访问元组中相应索引位置的元素,元素的索引从0开始计数。

代码清单2-5中,第2行代码声明的元组tup1包含3个元素,类型依次是i8类型、f32类型、bool类型。第3行代码声明的元组tup2包含两个元素,第1个元素是f64类型;第2个元素是元组类型,其中又包含两个元素,分别是bool类型和i32类型。第4行代码声明的元组tup3只包含一个元素。当元组中只包含一个元素时,应该在元素后面添加逗号来区分是元组,而不是括号表达式。第6、7行代码使用“元组名.索引”来访问元组中相应索引位置的元素。第9行代码使用模式匹配的方式来解构赋值,元组中的每个元素按照位置顺序赋值给变量,即tup1的3个元素分别解构赋值给变量x、y和z。

代码清单2-5 元组类型

 1  fn main() {
 2      let tup1: (i8, f32, bool) = (-10, 7.7, false);
 3      let tup2 = (7.7, (false, 10));
 4      let tup3 = (100, );
 5    
 6      println!("{}, {}", tup1.0, (tup2.1).1);
 7      println!("{}", tup3.0);
 8    
 9      let (x, y, z) = tup1;
10      println!("x: {}, y: {}, z: {}", x, y, z);
11  }
12
13  // -10, 10
14  // 100
15  // x: -10, y: 7.7, z: false

2.3.2 数组类型

数组类型是由相同类型的元素组合成的复合类型,我们可以使用[T; n]表示,T代表元素类型,n代表长度即元素个数。

数组的声明和初始化有以下3种方式。

1)指定数组类型,为每个元素赋初始值。所有初始值放入中括号“[]”中,之间使用逗号“,”分隔。

let arr: [i32; 5] = [1, 2, 3, 4, 5];

2)省略数组类型,为每个元素赋初始值。由于已指定每个元素的初始值,可以从初始值推断出数组类型。

let arr = [1, 2, 3, 4, 5];

3)省略数组类型,为所有元素使用默认值初始化。

let arr = [1; 5]; // 等价于:let arr = [1, 1, 1, 1, 1];

可以使用“数组名[索引]”来访问数组中相应索引位置的元素,元素的索引从0开始计数。

代码清单2-6中,第2行代码声明的数组arr1包含5个i32类型的元素并为每个元素赋初始值。第3行代码声明的数组arr2省略了数组类型,由编译器根据初始值自动推断数组类型。第4、5行代码使用默认值初始化数组,为每个元素指定初始值为1。

访问数组元素时最常遇到的问题是索引越界,第11行代码如果访问arr1[5],将会抛出index out of bounds: the len is 5 but the index is 5的错误提示。在实际项目开发中,建议使用更加灵活的动态数组Vec。Vec是允许增长和缩短长度的容器类型,其提供的get方法在访问元素时可以有效避免索引越界问题。

代码清单2-6 数组类型

 1  fn main() {
 2      let arr1: [i32; 5] = [1, 2, 3, 4, 5];
 3      let arr2 = [1, 2, 3, 4, 5];
 4      let arr3: [i32; 5] = [1; 5];
 5      let arr4 = [1; 5];
 6
 7      println!("{:?}", arr1);
 8      println!("{:?}", arr2);
 9      println!("{:?}", arr3);
10      println!("{:?}", arr4);
11      println!("arr1[0]: {}, arr3[2]: {}", arr1[0], arr3[2]);
12  }
13
14  // [1, 2, 3, 4, 5]
15  // [1, 2, 3, 4, 5]
16  // [1, 1, 1, 1, 1]
17  // [1, 1, 1, 1, 1]
18  // arr1[0]: 1, arr3[2]: 1

2.3.3 结构体类型

结构体类型是一个自定义数据类型,通过struct关键字加自定义命名,可以把多个类型组合在一起成为新的类型。结构体中以“name: type”格式定义字段,name是字段名称,type是字段类型。结构体名和字段名都遵循变量的命名规则,结构体名应该能够描述它所组合的数据的意义;字段默认不可变,并要求明确指定数据类型,不能使用自动类型推导功能。每个字段之间用逗号分隔,最后一个逗号可以省略。

结构体类型定义的语法如下。结构体ListNode是链表的数据结构,其中字段next的类型是一个指向ListNode结构体本身的智能指针。

1  struct ListNode {
2      val: i32,
3      next: Option<Box<ListNode>>,
4  }

代码清单2-7中,第1~4行代码定义的结构体Student包含&'static str类型的字段name和i32类型的字段score。第10~13行代码创建Student的实例student,以name: value格式为每个字段进行赋值,name是字段名,value是字段的值。第11行代码是字段初始化简写语法,将变量score的值赋值给字段score,两者有着相同的名称,可以简写成score,不用写成score: score。此外,实例中字段的顺序可以和结构体中声明的顺序不一致。

第15、16行代码使用“实例名.字段名”形式更改和访问结构体实例某个字段的值。需要注意的是,结构体实例默认是不可变的,且不允许只将某个字段标记为可变,如果要修改结构体实例必须在实例创建时就声明其为可变的。

第18~21行代码创建的实例student2,除字段name外,其余字段的值与student对应字段的值相同。这就可以使用结构体更新语法,对除字段name外未显式设置值的字段以student实例对应字段的值来赋值。

代码清单2-7 结构体类型

 1  struct Student {
 2      name: &'static str,
 3      score: i32,
 4  }
 5  
 6  fn main() {
 7      let score = 59;
 8      let username = "zhangsan";
 9
10      let mut student = Student {
11          score,
12          name: username,
13      };
14
15      student.score = 60;
16      println!("name: {}, score: {}", student.name, student.score);
17
18      let student2 = Student {
19          name: "lisi",
20          ..student
21      };
22
23      println!("name: {}, score: {}", student2.name, student2.score);
24  }
25
26  // name: zhangsan, score: 60
27  // name: lisi, score: 60

另外,还有两种特殊的结构体:元组结构体和单元结构体。

元组结构体的特点是字段只有类型,没有名称。元组结构体的定义以及实例创建如下所示。

1  struct Color(i32, i32, i32);
2  let black = Color(0, 0, 0);

单元结构体是指没有任何字段的结构体,代码如下所示,一般只用于特定的场景,在此不再细述。

struct Solution;

2.3.4 枚举类型

枚举类型是一个自定义数据类型,通过enum关键字加自定义命名来定义。其包含若干枚举值,可以使用“枚举名::枚举值”访问枚举值。当一个变量有几种可能的取值时,我们就可以将它定义为枚举类型。变量的值限于枚举值范围内,这样能有效防止用户提供无效值。根据枚举值是否带有类型参数,枚举类型还可以分成无参数枚举类型和带参数枚举类型。

代码清单2-8中,第2~6行代码定义了枚举类型ColorNoParam,包含Red、Yellow、Blue这3个枚举值。第9行代码声明的变量color_no_param与枚举值Red绑定。第10~14行代码使用match模式匹配来枚举所有的值,以处理不同值所对应的情况。第1行代码使用#[derive(Debug)]让ColorNoParam自动实现Debug trait,只有实现了Debug trait的类型才拥有使用{:?}格式化打印的行为,具体trait的相关知识会在第5章介绍。

代码清单2-8 无参数枚举类型

 1  #[derive(Debug)]
 2  enum ColorNoParam {
 3      Red,
 4      Yellow,
 5      Blue,
 6  }
 7
 8  fn main() {
 9      let color_no_param = ColorNoParam::Red;
10      match color_no_param {
11          ColorNoParam::Red => println!("{:?}", ColorNoParam::Red),
12          ColorNoParam::Yellow => println!("{:?}", ColorNoParam::Yellow),
13          ColorNoParam::Blue => println!("{:?}", ColorNoParam::Blue),
14      }
15  }
16
17  // Red

代码清单2-9中,枚举类型ColorParam的枚举值都带有String类型参数,类似于函数调用,使用这种枚举值需要传入实参。

代码清单2-9 带参数枚举类型

 1  #[derive(Debug)]
 2  enum ColorParam {
 3      Red(String),
 4      Yellow(String),
 5      Blue(String),
 6  }
 7
 8  fn main() {
 9      println!("{:?}", ColorParam::Blue(String::from("blue")));
10  }
11
12  // Blue("blue")