Appearance
2.3 复合数据类型
复合数据类型可以在其他类型的基础上形成更复杂的组合关系。
本章介绍 tuple、struct、enum 等几种复合数据类型。数组留到第 6 章介绍。
2.3.1 元组类型
tuple 指的是“元组”类型,它通过圆括号包含一组表达式构成。tuple 内的元素没有名字。tuple 是把几个类型组合到一起的最简单的方式。比如:
rust
let a = (1i32, false); // 元组中包含两个元素,第一个是 i32 类型,第二个是 bool 类型
let b = ("a", (1i32, 2i32)); // 元组中包含两个元素,第二个元素本身也是元组,它又包含了两个元素
如果元组中只包含一个元素,应该在后面添加一个逗号,以区分括号表达式和元组:
rust
let a = (0,); // a 是一个元组,它有一个元素
let b = (0); // b 是一个括号表达式,它是 i32 类型
访问元组内部元素有两种方法,一种是“模式匹配”(pattern destructuring),另外一种是“数字索引”:
rust
let p = (1i32, 2i32);
let (a, b) = p;
let x = p.0;
let y = p.1;
println!("{} {} {} {}", a, b, x, y);
在第 7 章中会对“模式匹配”做详细解释。
元组内部也可以一个元素都没有。这个类型单独有一个名字,叫 unit 类型(单元类型):
rust
let empty: () = ();
可以说,unit 类型是 Rust 中最简单的类型之一,也是占用空间最小的类型之一。 空元组和空结构体struct Foo;
一样,都是占用0
内存空间。
rust
fn main() {
println!("size of i8 {}" , std::mem::size_of::<i8>());
println!("size of char {}" , std::mem::size_of::<char>());
println!("size of '()' {}" , std::mem::size_of::<()>());
}
上面的程序中,std::mem::size_of
函数可以计算一个类型所占用的内存空间。 可以看到,i8 类型占用1
byte,char 类型占用4
bytes,空元组占用0
byte。
Rust 中存在实打实的0
大小的类型。这与 C++ 中的空类型不同,在 C++ 标准中,有明确的规定:
Complete objects and member subobjects of class type shall have nonzero size.
rust
class Empty {};
Empty emp;
assert(sizeof(emp) != 0);
2.3.2 结构体类型
结构体(struct)与元组类似,也可以把多个类型组合到一起,作为新的类型。区别在于,它的每个元素都有自己的名字。举个例子:
rust
struct Point {
x: i32,
y: i32,
}
每个元素之间采用逗号分开,最后一个逗号可以省略不写。类型依旧跟在冒号后面,但是不能使用自动类型推导功能,必须显式指定。 struct 类型的初始化语法类似于 json 的语法,使用“成员–冒号–值”的格式。
rust
fn main() {
let p = Point { x: 0, y: 0};
println!("Point is at {} {}", p.x, p.y);
}
有些时候,Rust 允许 struct 类型的初始化使用一种简化的写法。如果有局部变量名字和成员变量名字恰好一致,那么可以省略掉重复的冒号初始化:
rust
fn main() {
// 刚好局部变量名字和结构体成员名字一致
let x = 10;
let y = 20;
// 下面是简略写法,等同于 Point { x: x, y: y },同名字的相对应
let p = Point { x, y };
println!("Point is at {} {}", p.x, p.y);
}
访问结构体内部的元素,也是使用“点”加变量名的方式。当然,我们也可以使用“模式匹配”功能:
rust
fn main() {
let p = Point { x: 0, y: 0};
// 声明了 px 和 py,分别绑定到成员 x 和成员 y
let Point { x : px, y : py } = p;
println!("Point is at {} {}", px, py);
// 同理,在模式匹配的时候,如果新的变量名刚好和成员名字相同,可以使用简写方式
let Point { x, y } = p;
println!("Point is at {} {}", x, y);
}
Rust 设计了一个语法糖,允许用一种简化的语法赋值使用另外一个 struct 的部分成员。比如:
rust
struct Point3d {
x: i32,
y: i32,
z: i32,
}
fn default() -> Point3d {
Point3d { x: 0, y: 0, z: 0 }
}
// 可以使用 default() 函数初始化其他的元素
// ..expr 这样的语法,只能放在初始化表达式中,所有成员的最后最多只能有一个
let origin = Point3d { x: 5, ..default()};
let point = Point3d { z: 1, x: 2, ..origin };
如前所说,与 tuple 类似,struct 内部成员也可以是空:
rust
//以下三种都可以,内部可以没有成员
struct Foo1;
struct Foo2();
struct Foo3{}
关联函数
在 impl 块中定义的所有函数都被称为关联函数(associated functions),因为它们与 impl 后面命名的类型相关联。 我们也可以定义不以self
作为第一个参数的关联函数(因此不是方法),因为它们不需要类型的实例来使用。我们已经使用了一个这样的函数:String::from
函数,它是在 String 类型上定义的。
rust
// Define the struct using the struct keyword.
struct Rectangle {
width: u32,
height: u32,
}
// Declare the methods within an impl block associated with the struct.
impl Rectangle {
// new function to create a new Rectangle
fn new(width: u32, height: u32) -> Rectangle {
Rectangle { width, height }
}
// 计算矩形的面积
// Use the self parameter to access and modify the struct's data.
fn area(&self) -> u32 {
self.width * self.height
}
// 检查是否可以容纳另一个矩形
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
let rect2 = Rectangle::new(20, 40);
println!("矩形1的面积为:{}", rect1.area());
println!("矩形1是否可以容纳矩形2:{}", rect1.can_hold(&rect2));
}
2.3.3 tuple struct
Rust 有一种数据类型叫作 tuple struct,它就像是 tuple 和 struct 的混合。区别在于,tuple struct 有名字,而它们的成员没有名字:
rust
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
它们可以被想象成这样的结构体:
rust
struct Color{
0: i32,
1: i32,
2: i32,
}
struct Point {
0: i32,
1: i32,
2: i32,
}
因为这两个类型都有自己的名字,虽然它们的内部结构是一样的,但是它们是完全不同的两个类型。有时候我们不需要特别关心结构体内部成员的名字,可以采用这种语法。
tuple、struct、tuple struct 起的作用都是把几个不同类型的成员打包组合成一个类型。 它们的区别如下所示。
类型名称 | tuple | struct | tuple struct |
---|---|---|---|
语法 | 没名字圆括号 | 名字加大括号 | 名字加圆括号 |
类型名字 | 没有单独的名宇 | 有单独的名宇 | 有单独的名宇 |
成员名字 | 没有单独的名字 | 有单独的名字 | 没单独的名宇 |
它们除了在取名上有这些区别外,没有其他区别。它们有一致的内存对齐策略、一致的占用空间规则,也有类似的语法。从下面这个例子可以看出它们的语法是很一致的:
rust
// define struct
struct T1 {
v: i32
}
// define tuple struct
struct T2(i32);
fn main() {
let v1 = T1 { v: 1 };
let v2 = T2(1); // init tuple struct
let v3 = T2 { 0: 1 }; // init tuple struct
let i1 = v1.v;
let i2 = v2.0;
let i3 = v3.0;
}
tuple struct 有一个特别有用的场景,那就是当它只包含一个元素的时候,就是所谓的 newtype idiom。 因为它实际上让我们非常方便地在一个类型的基础上创建了一个新的类型。举例如下:
rust
fn main() {
struct Inches(i32);
fn f1(value : Inches) {}
fn f2(value : i32) {}
let v : i32 = 0;
f1(v); // 编译不通过,'mismatched types'
f2(v);
}
以上程序编译不通过,因为 Inches 类型和 i32 是不同的类型,函数调用参数不匹配。
但是,如果我们把以上程序改一下,使用 type alias(类型别名)实现,那么就可以编译通过了:
rust
fn type_alias() {
type I = i32;
fn f1(v : I) {}
fn f2(v : i32) {}
let v : i32 = 0;
f1(v);
f2(v);
}
从上面的讲解可以看出,通过关键字 type,我们可以创建一个新的类型名称,但是这个类型不是全新的类型,而只是一个具体类型的别名。 在编译器看来,这个别名与原先的具体类型是一模一样的。 而使用 tuple struct 做包装,则是创造了一个全新的类型,它跟被包装的类型不能发生隐式类型转换,可以具有不同的方法,满足不同的 trait,完全按需而定。
2.3.4 enum 类型
如果说 tuple、struct、tuple struct 在 Rust 中代表的是多个类型的“与”关系,那么 enum([ˈɪnəm
]:枚举)类型在 Rust 中代表的就是多个类型的“或”关系。
enum 是一种用于定义变体类型的语言结构。它的每项称为一个 variant(变体), 任何类型的数据都可以放入枚举成员中:例如字符串、数值、结构体甚至另一个枚举。
我们通过使用枚举名::枚举值
来访问枚举的值。 因此,不同的 enum 中重名的元素也不会互相冲突。 例如在下面的程序中,两个枚举内部都有Move
这个成员,但是它们不会有冲突。
rust
enum Message {
Quit,
ChangeColor(i32, i32, i32),
Move { x: i32, y: i32 },
Write(String),
}
let x: Message = Message::Move { x: 3, y: 4 };
enum BoardGameTurn {
Move { squares: i32 },
Pass,
}
let y: BoardGameTurn = BoardGameTurn::Move { squares: 1 };
我们也可以手动指定每个变体自己的标记值:
rust
fn main() {
enum Animal {
dog = 1,
cat = 200,
tiger,
}
let x = Animal::tiger as isize;
println!("{}", x);
}
使用场景:当一个参数可能存在多个取值范围时,我们定义成枚举类型来控制变量的取值范围,从而防止输入非法值。
rust
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let m1 = Message::Quit;
let m2 = Message::Move{x:1, y:1};
let m3 = Message::ChangeColor(255, 255, 0);
}
Rust 的 enum 中的每个元素的定义语法与 struct 的定义语法类似。可以像空结构体一样,不指定它的类型; 也可以像 tuple struct 一样,用圆括号加无名成员;还可以像正常结构体一样,用大括号加带名字的成员。
用 enum 把这些类型包含到一起之后,就组成了一个新的类型。
可在 enum 定义枚举类型的前面使用#[repr]
来指定枚举成员的数值范围,超出范围后将编译错误。 当不指定类型限制时,Rust 尽量以可容纳数据大小的最小类型。 例如,最大成员值为 100
,则用一个字节的 u8
类型,最大成员值为 500
,则用两个字节的 u16
。
rust
// 最大数值不能超过255
#[repr(u8)] // 限定范围为`0..=255`
enum E {
A,
B = 254,
C,
D, // 256,超过255,编译报错
}
要使用 enum,一般要用到“模式匹配”。模式匹配是很重要的一部分,用第 7 章来详细讲解。 这里我们给出一个用match
语句读取 enum 内部数据的示例:
rust
enum Number {
Int(i32),
Float(f32),
}
fn read_num(num: &Number) {
match num {
// 如果匹配到了 Number::Int 这个成员,那么 value 的类型就是 i32
&Number::Int(value) => println!("Integer {}", value),
// 如果匹配到了 Number::Float 这个成员,那么 value 的类型就是 f32
&Number::Float(value) => println!("Float {}", value),
}
}
fn main() {
let n: Number = Number::Int(10);
read_num(&n);
}
Rust 的 enum 与 C/C++ 的 enum 和 union 都不一样。它是一种更安全的类型,可以被称为“tagged union”。 从 C 语言的视角来看 Rust 的 enum 类型,重写上面这段代码,它的语义类似这样:
rust
#include <stdio.h>
#include <stdint.h>
// C 语言模拟 Rust 的 enum
struct Number {
enum {Int, Float} tag;
union {
int32_t int_value;
float float_value;
} value;
};
void read_num(struct Number * num) {
switch(num->tag) {
case Int:
printf("Integer %d", num->value.int_value);
break;
case Float:
printf("Float %f", num->value.float_value);
break;
default:
printf("data error");
break;
}
}
int main() {
struct Number n = { tag : Int, value: { int_value: 10} };
read_num(&n);
return 0;
}
Rust 的 enum 类型的变量需要区分它里面的数据究竟是哪种变体,所以它包含了一个内部的“tag 标记”来描述当前变量属于哪种类型。 这个标记对用户是不可见的,通过恰当的语法设计,保证标记与类型始终是匹配的,以防止用户错误地使用内部数据。
如果我们用 C 语言来模拟,就需要程序员自己来保证读写的时候标记和数据类型是匹配的,编译器无法自动检查。 当然,上面这个模拟只是为了通俗地解释 Rust 的 enum 类型的基本工作原理, 在实际中,enum 的内存布局未必是这个样子,编译器有许多优化,可以保证语义正确的同时减少内存使用,并加快执行速度。
如果是在 FFI 场景下,要保证 Rust 里面的 enum 的内存布局和 C 语言兼容的话, 可以给这个 enum 添加一个#[repr(C, Int)]
属性标签(目前这个设计已经通过,但是还未在编译器中实现)。
我们可以试着把前面定义的 Number 类型占用的内存空间大小打印出来看看:
rust
fn main() {
// 使用了泛型函数的调用语法,请参考第 21 章泛型
println!("Size of Number: {}", std::mem::size_of::<Number>());
println!("Size of i32: {}", std::mem::size_of::<i32>());
println!("Size of f32: {}", std::mem::size_of::<f32>());
}
编译执行结果:
txt
Size of Number: 8
Size of i32: 4
Size of f32: 4
Number
里面要么存储的是 i32,要么存储的是 f32,它存储数据需要的空间应该是max(sizeof(i32), sizeof(f32) = max(4 byte,4 byte) = 4 byte
。 而它总共占用的内存是8
byte,多出来的4
byte 就是用于保存类型标记的。之所以用4
byte,是为了内存对齐。
Rust 里面也支持 union 类型,这个类型与 C 语言中的 union 完全一致。 但在 Rust 里面,读取它内部的值被认为是 unsafe 行为,一般情况下我们不使用这种类型。 它存在的主要目的是为了方便与 C 语言进行交互。
Rust 标准库中有一个极其常用的 enum 类型Option<T>
,它的定义如下:
rust
pub enum Option<T> {
None,
Some(T),
}
由于它实在是太常用,标准库将 Option 以及它的成员Some
、None
都加入到了 Prelude 中,用户甚至不需要use
语句声明就可以直接使用。 它表示的含义是“要么存在、要么不存在”。 比如Option<i32>
表达的意思就是“可以是一个 i32 类型的值,或者没有任何值”。
Rust 的 enum 实际上是一种代数类型系统(Algebraic Data Type,ADT),本书第 8 章简要介绍什么是 ADT。
我们还可以将 enum 中的变体当做类型构造器使用。 意思是说,我们可以把 enum 内部的 variant 当成一个函数使用,示例如下:
rust
fn main() {
let arr = [1,2,3,4,5];
// 请注意这里的 map 函数
let v: Vec<Option<&i32>> = arr.iter().map(Some).collect();
println!("{:?}", v);
}
有关迭代器的知识,请各位读者参考第 24 章的内容。在这里想说明的问题是,Some 可以当成函数作为参数传递给map
。 这里的 Some 其实是作为一个函数来使用的,它输入的是&i32
类型,输出为Option<&i32>
类型。 可以用如下方式证明 Some 确实是一个函数类型,我们把 Some 初始化给一个 unit 变量,产生一个编译错误:
rust
fn main() {
let _ : () = Some;
}
编译错误是这样写的:
txt
| let _ : () = Some;
| -- ^^^^ expected `()`, found enum constructor
| |
| expected due to this
|
= note: expected unit type `()`
found enum constructor `fn(_) -> Option<_> {Option::<_>::Some}`
可见,enum 内部的 variant 的类型确实是函数类型。
enum 和 struct 之间还有一个相似之处:就像我们可以使用impl
在结构体上定义方法一样,我们也可以在 enum 上定义方法。这里有一个名为display_mood
的方法,我们可以在Mood
枚举上定义:
rust
// Define the enum using the enum keyword.
enum Mood {
Happy,
Sad,
}
// Declare methods within an impl block associated with the enum.
impl Mood {
// 输出当前心情
// Use pattern matching (match) to handle each variant of the enum and implement specific behavior for each case.
fn display_mood(&self) {
match self {
Mood::Happy => println!("我很开心!"),
Mood::Sad => println!("我很伤心。"),
}
}
}
fn main() {
let my_mood = Mood::Happy;
my_mood.display_mood();
}
2.3.5 类型递归定义
Rust 里面的复合数据类型是允许递归定义的。比如 struct 里面嵌套同样的 struct 类型,但是直接嵌套是不行的。示例如下:
rust
struct Recursive {
data: i32,
rec: Recursive,
}
使用rustc --crate-type=lib test.rs
命令编译,可以看到如下编译错误:
error[E0072]: recursive type `Recursive` has infinite size
--> test.rs:2:1
|
2 | struct Recursive {
| ^^^^^^^^^^^^^^^^ recursive type has infinite size
3 | data: i32,
4 | rec: Recursive,
| -------------- recursive without indirection
|
= help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to make `Recursive` representable
以上编译错误写得非常人性化,不仅写清楚了错误原因,还给出了可能的修复办法。Rust 是允许用户手工控制内存布局的语言。 直接使用类型递归定义的问题在于,当编译器计算 Recursive 这个类型大小的时候:
rust
size_of::<Recursive>() == 4 + size_of::<Recursive>()
这个方程在实数范围内无解。
解决办法很简单,用指针间接引用就可以了,因为指针的大小是固定的,比如:
rust
struct Recursive {
data: i32,
rec: Box<Recursive>,
}
我们把产生了递归的那个成员类型改为了指针,这个类型就非常合理了。