Rust “类型”

0x01. 标量类型

标量类型表示单个值。Rust 有四种主要标量类型:整数、浮点数、布尔值和字符。

1.1 整数类型

Rust 中的整数类型:

Length Signed Unsigned
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
arch isize usize

Rust 中的整数字面量:

Numberliterals Example
Decimal 98_222
Hex 0xff
Octal 0o77
Binary 0b1111_0000
Byte (u8 only) b'A'

1.2 浮点类型

  • Rust 还具有两种浮点数基本类型,即带有小数点的数字。
  • Rust 的浮点类型是 f32 和 f64,大小分别为 32 位和 64 位。
  • 默认类型为 f64,因为在现代 CPU 上,它的速度与 f32 大致相同,但精度更高。
  • 所有浮点类型都是有符号的。

1.3 布尔类型

  • Rust 中的布尔类型有两个可能的值:true 和 false。
  • 布尔值的大小为一个字节。

Rust 中的布尔类型使用 bool 指定。例如:

1
2
3
4
5
fn main() {
let t = true;

let f: bool = false; // with explicit type annotation
}

1.4 字符类型

Rust 的 char 类型是该语言最原始的字母类型。以下是声明 char 值的一些示例:

1
2
3
4
5
fn main() {
let c = 'z';
let z: char = 'ℤ'; // with explicit type annotation
let heart_eyed_cat = '😻';
}

请注意,我们用单引号指定 char 文字,而不是使用双引号的字符串文字。Rust 的 char 类型大小为四个字节,表示 Unicode 标量值,这意味着它可以表示的不仅仅是 ASCII。重音字母;中文、日文和韩文字符;表情符号;以及零宽度空格都是 Rust 中的有效 char 值。

Unicode 标量值的范围从 U+0000 到 U+D7FF 和 U+E000 到 U+10FFFF (含)。但是,“字符”在 Unicode 中并不是一个真正的概念,因此你对“字符”的人类直觉可能与 Rust 中的 char 不匹配。我们将在第 8 章“使用字符串存储 UTF-8 编码文本 ”中详细讨论这个主题。

0x02. 字符串类型

看一下图 4-1,了解 String 内部发生了什么。String 由三部分组成,如左侧所示:指向保存字符串内容的内存的指针、长度和容量。这组数据存储在堆栈上。右侧是保存内容的堆上的内存。

2.1 创建字符串

1
let s = String::from("hello");

0x03. 复合类型

3.2 数组

另一种拥有多个值集合的方法是使用数组。与元组不同,数组的每个元素都必须具有相同的类型。与其他一些语言中的数组不同,Rust 中的数组具有固定长度

1
2
3
fn main() {
let a = [1, 2, 3, 4, 5];
}

您可以使用方括号来编写数组的类型,其中包含每个元素的类型、分号,然后是数组中元素的数量,如下所示:

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

您还可以通过指定初始值,后跟分号,然后在方括号中指定数组的长度,来初始化一个数组,使其每个元素包含相同的值,如下所示:

1
2
let a = [3; 5];
// 这与 let a = [3, 3, 3, 3, 3]; 的写法相同,但更简洁。

数组是一块已知且固定大小的内存,可在堆栈上分配。您可以使用索引来访问数组的元素,如下所示:

1
2
3
4
5
6
fn main() {
let a = [1, 2, 3, 4, 5];

let first = a[0];
let second = a[1];
}

3.1 元组

元组是一种将多个具有各种类型的值组合成一种复合类型的通用方法。元组的长度是固定的:一旦声明,它们的大小就不能增加或缩小。

我们通过在括号内写入逗号分隔的值列表来创建元组。元组中的每个位置都有一个类型,并且元组中不同值的类型不必相同。我们在此示例中添加了可选的类型注释:

1
2
3
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}

变量 tup 绑定到整个元组,因为元组被视为单个复合元素。要从元组中获取单个值,我们可以使用模式匹配来解构元组值,如下所示:

1
2
3
4
5
6
7
fn main() {
let tup = (500, 6.4, 1);

let (x, y, z) = tup;

println!("The value of y is: {y}");
}

我们还可以使用句点 (.) 后跟要访问的值的索引来直接访问元组元素。例如:

1
2
3
4
5
6
7
8
9
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;
}

没有任何值的元组有一个特殊名称,即 unit。此值及其对应的类型都写为 (),表示空值或空返回类型。如果表达式不返回任何其他值,则隐式返回 unit 值。

3.3 结构体

结构体类似于“元组类型”部分中讨论的元组,因为两者都包含多个相关值。与元组一样,结构体的各个部分可以是不同的类型。与元组不同,在结构体中,您将命名每个数据部分,以便清楚地了解值的含义。添加这些名称意味着结构体比元组更灵活:您不必依赖数据的顺序来指定或访问实例的值。

要定义结构体,我们输入关键字 struct 并命名整个结构体。结构体的名称应该描述将数据组合在一起的意义。然后,在花括号内,我们定义数据的名称和类型,我们称之为字段。例如,清单 5-1 显示了一个存储有关用户帐户信息的结构体。

1
2
3
4
5
6
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}

要在定义结构体后使用它,我们需要通过为每个字段指定具体值来创建该结构的实例。我们通过声明结构体的名称来创建实例,然后添加包含键:值对的大括号,其中键是字段的名称,值是我们想要存储在这些字段中的数据。我们不必按照在结构体中声明字段的顺序指定字段。换句话说,结构体定义就像类型的通用模板,实例用特定数据填充该模板以创建类型的值。例如,我们可以声明一个特定的用户,如示例 5-2 所示。

1
2
3
4
5
6
7
8
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
}

要从结构体中获取特定值,我们使用点符号。例如,要访问此用户的电子邮件地址,我们使用 user1.email。如果实例是可变的,我们可以使用点符号并赋值到特定字段来更改值。清单 5-3 显示了如何更改可变 User 实例的 email 字段中的值。

1
2
3
4
5
6
7
8
9
10
fn main() {
let mut user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};

user1.email = String::from("anotheremail@example.com");
}

清单 5-4 显示了 build_user 函数,该函数返回具有给定电子邮件和用户名的 User 实例。active 字段的值为 true,sign_in_count 的值为 1。

1
2
3
4
5
6
7
8
fn build_user(email: String, username: String) -> User {
User {
active: true,
username: username,
email: email,
sign_in_count: 1,
}
}

将函数参数命名为与结构字段相同的名称是有意义的,但必须重复电子邮件和用户名字段名称和变量有点乏味。如果结构有更多字段,重复每个名称会变得更加烦人。幸运的是,有一个方便的简写!

1
2
3
4
5
6
7
8
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}

这里,我们创建了 User 结构体的一个新实例,该结构体有一个名为 email 的字段。我们想将 email 字段的值设置为 build_user 函数的 email 参数中的值。由于 email 字段和 email 参数同名,因此我们只需要写 email 而不是 email: email。

3.3.1 结构体更新语法

1
2
3
4
5
6
7
8
9
10
fn main() {
// --snip--

let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
}
1
2
3
4
5
6
7
8
fn main() {
// --snip--

let user2 = User {
email: String::from("another@example.com"),
..user1
};
}

清单 5-7 中的代码还在 user2 中创建了一个实例,该实例的 email 值与 user1 不同,但 username、active 和 sign_in_count 字段的值与 user1 相同。..user1 必须放在最后,以指定任何剩余字段应从 user1 中的相应字段中获取其值,但我们可以选择以任意顺序为任意数量的字段指定值,而不管结构定义中字段的顺序如何。

3.3.2 元组结构体

Rust 还支持类似于元组的结构,称为元组结构。元组结构具有结构名称提供的附加含义,但没有与其字段关联的名称;相反,它们只有字段的类型。当你想给整个元组命名并使元组与其他元组的类型不同,并且像在常规结构中一样命名每个字段会很冗长或冗余时,元组结构很有用。

1
2
3
4
5
6
7
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}

3.3.3 单元结构体

您还可以定义没有任何字段的结构体!这些被称为类单元结构,因为它们的行为类似于 (),后者是我们在“元组类型”部分中提到的单元类型。当您需要在某种类型上实现特征但没有任何要存储在类型本身中的数据时,类单元结构会很有用。以下是声明和实例化名为 AlwaysEqual 的单元结构体的示例:

1
2
3
4
5
struct AlwaysEqual;

fn main() {
let subject = AlwaysEqual;
}

3.4 枚举

我们可以通过定义 IpAddrKind 枚举并列出 IP 地址可能的类型 V4 和 V6,在代码中表达这一概念。这些是枚举的变体:

1
2
3
4
enum IpAddrKind {
V4,
V6,
}

我们可以像这样创建 IpAddrKind 的两个变体的实例:

1
2
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

请注意,枚举的变体在其标识符下命名,我们使用双冒号将两者分开。这很有用,因为现在 IpAddrKind::V4 和 IpAddrKind::V6 这两个值都属于同一类型:IpAddrKind。然后,例如,我们可以定义一个接受任何 IpAddrKind 的函数:

1
2
3
4
5
fn route(ip_kind: IpAddrKind) {}

// 我们可以用以下任一方式调用该函数:
route(IpAddrKind::V4);
route(IpAddrKind::V6);

使用枚举而不是结构还有另一个优势:每个变体可以具有不同类型和数量的关联数据。版本 4 IP 地址将始终具有四个数字组件,其值介于 0 到 255 之间。如果我们想将 V4 地址存储为四个 u8 值,但仍将 V6 地址表示为一个字符串值,则无法使用结构。枚举可以轻松处理这种情况:

1
2
3
4
5
6
7
8
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);

let loopback = IpAddr::V6(String::from("::1"));

让我们看一下示例 6-2 中的另一个枚举示例:这个示例的变体中嵌入了多种类型。

1
2
3
4
5
6
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}

此枚举有四种不同类型的变体:

  • Quit 根本没有与之关联的数据。
  • Move 有命名字段,就像结构一样。
  • Write 包含单个字符串。
  • ChangeColor 包含三个 i32 值。

3.4.1 Option 枚举

1
2
3
4
enum Option<T> {
None,
Some(T),
}

3.4.2 Result 枚举

1
2
3
4
enum Result<T, E> {
Ok(T),
Err(E),
}

0x04. 集合类型

4.1 向量 Vec<T>

我们将要介绍的第一个集合类型是 Vec<T>,也称为向量。向量允许您在单个数据结构中存储多个值,该数据结构将所有值并排放置在内存中。向量只能存储相同类型的值。当您有项目列表时,它们很有用,例如文件中的文本行或购物车中项目的价格。

4.1.1 创建新向量

为了创建一个新的空向量,我们调用 Vec::new 函数,如示例 8-1 所示。

1
let v: Vec<i32> = Vec::new();

更常见的情况是,你会创建一个带有初始值的 Vec,Rust 会推断出你想要存储的值的类型,所以你很少需要做这种类型注释。Rust 方便地提供了 vec! 宏,它将创建一个新的向量来保存你给它的值。示例 8-2 创建了一个新的 Vec,它保存了值 1、2 和 3。整数类型是 i32,因为这是默认的整数类型,正如我们在第 3 章的“数据类型”部分讨论的那样。

1
let v = vec![1, 2, 3];

4.1.2 更新向量

1
2
3
4
5
6
let mut v = Vec::new();

v.push(5);
v.push(6);
v.push(7);
v.push(8);

4.1.3 读取向量元素

1
2
3
4
5
6
7
8
9
10
11
let v = vec![1, 2, 3, 4, 5];

let third: &i32 = &v[2];
println!("The third element is {third}");

let third: Option<&i32> = v.get(2);
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}

4.1.4 迭代向量中的值

1
2
3
4
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}

我们还可以迭代可变向量中每个元素的可变引用,以便更改所有元素。示例 8-8 中的 for 循环将为每个元素添加 50。

1
2
3
4
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}

要更改可变引用所指的值,我们必须先使用 * 解引用运算符来获取 i 中的值,然后才能使用 += 运算符。我们将在第 15 章的“使用解引用运算符跟踪指针指向值”部分中进一步讨论解引用运算符。

4.1.5 使用枚举存储多种类型

1
2
3
4
5
6
7
8
9
10
11
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}

let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];

4.2 哈希映射 HashMap<K, V>

4.2.1 创建新的 HashMap<K, V>

1
2
3
4
5
6
use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

4.2.2 访问哈希映射中的值

1
2
3
4
5
6
7
8
9
use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);

我们可以使用与向量类似的方式,使用 for 循环遍历哈希图中的每个键值对:

1
2
3
4
5
6
7
8
9
10
use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

for (key, value) in &scores {
println!("{key}: {value}");
}