时间轴

2025-09-11

init

2025-10-19

modify some format errors

2025-10-22

move something to another post


安装与调试

安装

Linux 情况下

1
2
3
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 按照提示进行安装
rustc --version

调试

Linux 下使用

1
rust-gdb target/debug/your_program

Cargo

创建项目

cargo new 项目名称

帮助信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Create a new cargo package at <path>

Usage: cargo.exe new [OPTIONS] <path>

Arguments:
<path>

Options:
-q, --quiet Do not print cargo log messages
--registry <REGISTRY> Registry to use
--vcs <VCS> Initialize a new repository for the given version control system (git, hg, pijul, or fossil) or do not initialize any version control at all (none), overriding a global configuration. [possible values: git, hg, pijul, fossil, none]
--bin Use a binary (application) template [default]
-v, --verbose... Use verbose output (-vv very verbose/build.rs output)
--lib Use a library template
--color <WHEN> Coloring: auto, always, never
--edition <YEAR> Edition to set for the crate generated [possible values: 2015, 2018, 2021]
--frozen Require Cargo.lock and cache are up to date
--name <NAME> Set the resulting package name, defaults to the directory name
--locked Require Cargo.lock is up to date
--offline Run without accessing the network
--config <KEY=VALUE> Override a configuration value
-Z <FLAG> Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details
-h, --help Print help information

Run `cargo help new` for more detailed information.

Cargo.toml

TOML(Tom’s Obvious,Minimal Language)格式,是 Cargo 的配置格式

1
2
3
4
5
6
7
8
9
[package]#区域标题,表示一下面是用来配置包package的
name = "hello" #项目名称
version = "0.1.0" #项目版本
authors = ["cauchy <731005515@qq.com>"] #作者
edition = "2021" #使用的Rust版本

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]#依赖项

在 Rust 中,代码的包叫做crate

构建 Cargo 项目

1
cargo build

创建可执行文件 target/debug/hello_cargo 或 target\debug\hello_cargo.exe(Windows)

运行.\target\debug\hello_cargo.exe

第一次运行会生成 cargo.lock 文件

该文件负责追踪项目依赖的精确版本,不需要手动修改该文件

构建和运行 Cargo 项目

1
cargo run

如果之前编译过且代码没有修改的话会直接执行

cargo check

1
cargo check

检查代码,确保能通过编译,但是不产生任何可执行文件

cargo check 比 cargo build 快得多

发布构建

1
cargo build --release

编译时会进行优化,代码运行的更快但是编译时间更长

会在 target/release 而不是 target/debug 生成可执行文件

变量与可变性

变量

声明使用let关键字

默认情况下,变量是不可变的(immutable)

声明变量时,前面加上mut关键字,就可以使变量可变

let mut x = 3;

常量

类似于不可变变量,常量(constants) 是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。

1.不允许对常量使用 mut。常量不光默认不能变,它总是不能变。

2.声明常量使用 const 关键字而不是 let,并且 必须 注明值的类型

3.常量可以在任何作用域中声明,包括全局作用域,

4.最后一个区别是,常量只能被设置为常量表达式,而不可以是其他任何只能在运行时计算出的值

1
2
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
const MAX_POINTS:u32 = 100_000;

命名规范:全部大写,下划线间隔

隐藏(shadow)

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let x = 5;

let x = x + 1;

{
let x = x * 2;
println!("The value of x in the inner scope is: {x}");
}

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

我们可以定义一个与之前变量同名的新变量,新的变量会 shadow 之前声明的同名变量

shadow 和把变量标记为 mut是不一样的

  • 如果不适用 let 关键字,那么给非 mut 的变量赋值会导致编译时错误
  • 使用 let 声明的同名新变量,也是不可变的
  • 使用 let 声明的同名新变量,他的类型可以与之前不同

允许未使用的变量

两种方式

1
2
3
4
5
6
7
fn main() {
let _x = 1;
}
#[allow(unused_variables)]
fn main() {
let x = 1;
}

数据类型

Rust 是静态编译语言,编译时必须知道所有变量的类型

标量类型

Rust 有四种基本的标量类型:整型浮点型布尔类型字符类型

整数类型

如果我们没有显式的给予变量一个类型,那编译器会自动帮我们推导一个类型

1
2
3
4
5
6
7
8
fn main(){
let x = 5;
assert_eq!("i32".to_string(),type_of(&x));
}
// 以下函数可以获取传入参数的类型,并返回类型的字符串形式
fn type_of<T>(_: &T) -> String{
format!("{}",std::any::type_name::<T>())
}

整数如果不赋予类型默认为 i32 类型

1
2
3
fn main() {
let v: u16 = 38_u8 as u16;
}
长度 有符号 无符号
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
arch isize usize

isize 和 usize 类型依赖运行程序的计算机架构:64 位架构上它们是 64 位的, 32 位架构上它们是 32 位的。

1
2
3
4
fn main() {
assert_eq!(i8::MAX, 127);
assert_eq!(u8::MAX, 255);
}

整型字面值

数字字面值 例子
Decimal (十进制) 98_222
Hex (十六进制) 0xff
Octal (八进制) 0o77
Binary (二进制) 0b1111_0000
Byte (单字节字符)(仅限于 u8) b’A’

Rust 的数字类型默认是 i32。isize 或 usize 主要作为某些集合的索引。

整形溢出

比方说有一个 u8 ,它可以存放从零到 255 的值。那么当你将其修改为 256 时会发生什么呢?这被称为 “整型溢出”(integer overflow ),这会导致以下两种行为之一的发生。当在 debug 模式编译时,Rust 检查这类问题并使程序 panic,这个术语被 Rust 用来表明程序因错误而退出。

在 release 构建中,Rust 不检测溢出,相反会进行一种被称为二进制补码回绕(two’s complement wrapping)的操作。简而言之,比此类型能容纳最大值还大的值会回绕到最小值,值 256 变成 0,值 257 变成 1,依此类推。依赖整型回绕被认为是一种错误,即便可能出现这种行为。如果你确实需要这种行为,标准库中有一个类型显式提供此功能,Wrapping。 为了显式地处理溢出的可能性,你可以使用标准库在原生数值类型上提供的以下方法:

  • 所有模式下都可以使用 wrapping_* 方法进行回绕,如 wrapping_add
  • 如果 checked_* 方法出现溢出,则返回 None 值
  • overflowing_* 方法返回值和一个布尔值,表示是否出现溢出
  • saturating_* 方法在值的最小值或最大值处进行饱和处理
1
2
3
4
5
6
// 解决代码中的错误和 `panic`
fn main() {
let v1 = 251_u8 + 8;
let v2 = i8::checked_add(251, 8).unwrap();
println!("{},{}",v1,v2);
}

修改

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let v1 = 247_u8 + 8;
let v2 = i8::checked_add(119, 8).unwrap();
println!("{},{}",v1,v2);
}
#[allow(unused_variables)]
fn main() {
let v1 = 251_u16 + 8;
let v2 = u16::checked_add(251, 8).unwrap();
println!("{},{}",v1,v2);
}

浮点类型

Rust 的浮点数类型是 f32 和 f64,分别占 32 位和 64 位。默认类型是 f64,因为在现代 CPU 中,它与 f32 速度几乎一样,不过精度更高。所有的浮点型都是有符号的。

1
2
3
4
5
fn main() {
let x = 2.0; // f64

let y: f32 = 3.0; // f32
}

浮点数采用 IEEE-754 标准表示。f32 是单精度浮点数,f64 是双精度浮点数。

1
2
3
4
5
fn main() {
let x = 1_000.000_1; // f64
let y: f32 = 0.12; // f32
let z = 0.01_f64; // f64
}

数值运算

Rust 中的所有数字类型都支持基本数学运算:加法、减法、乘法、除法和取余。整数除法会向下舍入到最接近的整数

1
2
3
4
5
6
fn main() {
assert_eq!(0.1+0.2,0.3);//报错
}
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `0.30000000000000004`,
right: `0.3`', src\main.rs:5:5

两种修改方法

1
2
3
4
5
6
fn main() {
assert!(0.1+0.2>=0.3);
}
fn main() {
assert!(0.1_f32+0.2_f32==0.3_f32);
}

计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
use std::fmt::Display;

#[allow(unused_variables)]
fn print_something<T >(something:T)
where T : Display
{
println!("{}",something);
}
fn main() {
// 整数加法
print_something(1u32 + 2 );

// 整数减法
print_something(1i32 - 2 );
print_something(1i8 - 2);

print_something(3 * 50 );

print_something(9 / 3 == 3); // error ! 修改它让代码工作

print_something(24 % 5 );

// 逻辑与或非操作
print_something(true && false );
print_something(true || false );
print_something(!true );

// 位操作
println!("0011 AND 0101 is {:04b}", 0b0011u32 & 0b0101);
println!("0011 OR 0101 is {:04b}", 0b0011u32 | 0b0101);
println!("0011 XOR 0101 is {:04b}", 0b0011u32 ^ 0b0101);
println!("1 << 5 is {}", 1u32 << 5);
println!("0x80 >> 2 is 0x{:x}", 0x80u32 >> 2);
}

序列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
let mut sum = 0;
for i in -3..2 {
println!("i is {}",i);//-3到1不包括2
}

for c in 'a'..='z' {
println!("{}",c);//a-z包括z
}
}
#[allow(unused_variables)]
// 解决代码中的错误和 `panic`
use std::ops::{Range, RangeInclusive};
fn main() {
assert_eq!((1..5), Range{ start: 1, end: 5 });
assert_eq!((1..=5), RangeInclusive::new(1, 5));
}

布尔类型

Rust 中的布尔类型使用 bool 表示

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let t = true;

let f: bool = false; // with explicit type annotation
}
fn main() {
let f = true;
let t = true && false || true;//布尔运算
assert_eq!(t, f);

println!("Success!")
}

字符类型

Rust 的 char 类型是语言中最原生的字母类型

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let c = 'z';
let z: char = 'ℤ'; // with explicit type annotation
let heart_eyed_cat = '😻';
}
fn main() {
let c1 = '中';
print_char(c1);
}

fn print_char(c : char) {
println!("{}", c);
}

用单引号声明 char 字面量,而与之相反的是,使用双引号声明字符串字面量。Rust 的 char 类型的大小为四个字节(four bytes),并代表了一个 Unicode 标量值(Unicode Scalar Value),这意味着它可以比 ASCII 表示更多内容。在 Rust 中,带变音符号的字母(Accented letters),中文、日文、韩文等字符,emoji(绘文字)以及零长度的空白字符都是有效的 char 值。Unicode 标量值包含从 U+0000 到 U+D7FF 和 U+E000 到 U+10FFFF 在内的值。不过,“字符” 并不是一个 Unicode 中的概念,所以人直觉上的 “字符” 可能与 Rust 中的 char 并不符合。

大小

1
2
3
4
5
6
7
8
9
10
11
12
#[allow(unused_variables)]

use std::mem::size_of_val;
fn main() {
let c1 = 'a';
assert_eq!(size_of_val(&c1),4); //一个字符4个字节

let c2 = '中';
assert_eq!(size_of_val(&c2),4);

println!("Success!")
}

单元类型

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let _v: () = ();

let v = (2, 3);
assert_eq!(_v, implicitly_ret_unit());

println!("Success!")
}

fn implicitly_ret_unit() {
println!("I will return a ()")
}

单元类型所占的内存为 0!!!

1
2
3
4
5
6
7
use std::mem::size_of_val;
fn main() {
let unit: () = ();
assert!(size_of_val(&unit) == 0);

println!("Success!")
}

复合类型

复合类型Compound types)可以将多个值组合成一个类型。Rust 有两个原生的复合类型:元组(tuple)和数组(array)。

元组类型

元组长度固定:一旦声明,其长度不会增大或缩小

使用包含在圆括号中的逗号分隔的值列表来创建一个元组。元组中的每一个位置都有一个类型,而且这些不同值的类型也不必是相同的

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

tup 变量绑定到整个元组上,因为元组是一个单独的复合元素。为了从元组中获取单个值,可以使用模式匹配(pattern matching)来解构(destructure)元组值,像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
let tup = (500, 6.4, 1);

let (x, y, z) = tup;

println!("The value of y is: {y}");
}
fn main() {
let (x, y, z);

// 填空
(y,z,x) = (1, 2, 3);

assert_eq!(x, 3);
assert_eq!(y, 1);
assert_eq!(z, 2);
}

程序首先创建了一个元组并绑定到 tup 变量上。接着使用了 let 和一个模式将 tup 分成了三个不同的变量,x、y 和 z。这叫做 解构destructuring),因为它将一个元组拆成了三个部分。

也可以使用点号(.)后跟值的索引来直接访问它们。元组的第一个索引值是 0。例如:

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) 元组。这种值以及对应的类型都写作 (),表示空值或空的返回类型。如果表达式不返回任何其他值,则会隐式返回单元值。

过长的元组无法打印

1
2
3
4
5
6
7
8
9
10
// 修复代码错误
fn main() {
let too_long_tuple = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13);
println!("too long tuple: {:?}", too_long_tuple);
}
//修复
fn main() {
let too_long_tuple = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
println!("too long tuple: {:?}", too_long_tuple);
}

数组类型

与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,Rust 中的数组长度是固定的。

数组的类型是[T; Length]数组的长度是类型签名的一部分,因此数组的长度必须在编译期就已知,

vector 类型是标准库提供的一个 允许 增长和缩小长度的类似数组的集合类型。当不确定是应该使用数组还是 vector 的时候,那么很可能应该使用 vector。

可以像这样编写数组的类型:在方括号中包含每个元素的类型,后跟分号,再后跟数组元素的数量。

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

这里,i32 是每个元素的类型。分号之后,数字 5 表明该数组包含五个元素。

1
2
3
4
5
6
7
8
9
fn main() {
// 很多时候,我们可以忽略数组的部分类型,也可以忽略全部类型,让编译器帮助我们推导
let arr0 = [1, 2, 3];
let arr: [char; 3] = ['a', 'b', 'c'];

// 数组分配在栈上, `std::mem::size_of_val` 函数会返回整个数组占用的内存空间
// 数组中的每个 char 元素占用 4 字节的内存空间,因为在 Rust 中, char 是 Unicode 字符
assert!(std::mem::size_of_val(&arr) == 12);
}

还可以通过在方括号中指定初始值加分号再加元素个数的方式来创建一个每个元素都为相同值的数组

1
let a = [3; 5];

变量名为 a 的数组将包含 5 个元素,这些元素的值最初都将被设置为 3。这种写法与 let a = [3, 3, 3, 3, 3]; 效果相同,但更简洁。

访问数组元素

数组是可以在栈(stack)上分配的已知固定大小的单个内存块。可以使用索引来访问数组的元素,像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {
let a = [1, 2, 3, 4, 5];

let first = a[0];
let second = a[1];
}
fn main() {
let names = [String::from("Sunfei"), "Sunface".to_string()];

// `get` 返回 `Option<T>` 类型,因此它的使用非常安全
let name0 = names.get(0).unwrap();

// 但是下标索引就存在越界的风险了
let _name1 = &names[1];
}

无效的数组访问

如果我们访问数组结尾之后的元素,程序在索引操作中使用一个无效的值时导致 运行时 错误。程序带着错误信息退出。当尝试用索引访问一个元素时,Rust 会检查指定的索引是否小于数组的长度。如果索引超出了数组长度,Rust 会 panic,这是 Rust 术语,它用于程序因为错误而退出的情况。

这种检查必须在运行时进行,特别是在某些情况下,因为编译器不可能知道用户在以后运行代码时将输入什么值。

类型转换

使用 as 进行基本类型转换

1.Rust 并没有为基本类型提供隐式的类型转换( coercion ),但是我们可以通过 as 来进行显式地转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#[allow(unused_variables)]
fn main() {
let decimal = 97.123_f32;

let integer: u8 = decimal as u8;

let c1: char = decimal as u8 as char;
let c2 = integer as char;

println!("c1 is {}",c1);
assert_eq!(integer, 'b' as u8 - 1);

println!("Success!")
}

2.默认情况下, 数值溢出会导致编译错误,但是我们可以通过添加一行全局注解 #![allow(overflowing_literals)] 的方式来避免编译错误(溢出还是会发生)

1
2
3
4
5
#![allow(overflowing_literals)]
fn main() {
assert_eq!(u8::MAX, 255);
let v = 1000 as u8;
}

3.当将任何数值转换成无符号整型 T 时,如果当前的数值不在新类型的范围内,我们可以对当前数值进行加值或减值操作( 增加或减少 T::MAX + 1 ),直到最新的值在新类型的范围内,假设我们要将 300 转成 u8 类型,由于 u8 最大值是 255,因此 300 不在新类型的范围内并且大于新类型的最大值,因此我们需要减去 T::MAX + 1,也就是 300 - 256 = 44。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#![allow(overflowing_literals)]
fn main() {
assert_eq!(1000 as u16, 1000);

assert_eq!(1000 as u8, 232);

// 事实上,之前说的规则对于正整数而言,就是如下的取模
println!("1000 mod 256 is : {}", 1000 % 256);

assert_eq!(-1_i8 as u8, 255);

// 从 Rust 1.45 开始,当浮点数超出目标整数的范围时,转化会直接取正整数取值范围的最大或最小值
assert_eq!(300.1_f32 as u8, 255);
assert_eq!(-100.1_f32 as u8, 0);


// 上面的浮点数转换有一点性能损耗,如果对于某段代码有极致的性能要求,
// 可以考虑下面的方法,但是这些方法的结果可能会溢出并且返回一些无意义的值
// 总之,请小心使用
unsafe {
// 300.0 is 44
println!("300.0 is {}", 300.0_f32.to_int_unchecked::<u8>());
// -100.0 as u8 is 156
println!("-100.0 as u8 is {}", (-100.0_f32).to_int_unchecked::<u8>());
// nan as u8 is 0
println!("nan as u8 is {}", f32::NAN.to_int_unchecked::<u8>());
}
}

4.裸指针可以和代表内存地址的整数互相转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn main() {
let mut values: [i32; 2] = [1, 2];
let p1: *mut i32 = values.as_mut_ptr();
let first_address = p1 as usize;
let second_address = first_address + 4; // 4 == std::mem::size_of::<i32>()
let p2 = second_address as *mut i32;
unsafe {
*p2 += 1;
}
assert_eq!(values[1], 3);

println!("Success!")
}
fn main() {
let arr :[u64; 13] = [0; 13];
assert_eq!(std::mem::size_of_val(&arr), 8 * 13);
let a: *const [u64] = &arr;
let b = a as *const [u8];
unsafe {
assert_eq!(std::mem::size_of_val(&*b), 13)
}
}

From/Into

  1. From 特征允许让一个类型定义如何基于另一个类型来创建自己,因此它提供了一个很方便的类型转换的方式。
  2. From 和 Into 是配对的,我们只要实现了前者,那后者就会自动被实现:只要实现了 impl From for U, 就可以使用以下两个方法: let u: U = U::from(T) 和 let u:U = T.into(),前者由 From 特征提供,而后者由自动实现的 Into 特征提供。
  3. 需要注意的是,当使用 into 方法时,需要进行显式地类型标注,因为编译器很可能无法帮我们推导出所需的类型。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
fn main() {
let my_str = "hello";

// 以下三个转换都依赖于一个事实:String 实现了 From<&str> 特征
let string1 = String::from(my_str);
let string2 = my_str.to_string();
// 这里需要显式地类型标注
let string3: String = my_str.into();
}
fn main() {
// impl From<bool> for i32
let i1:i32 = false.into();
let i2:i32 = i32::from(false);
assert_eq!(i1, i2);
assert_eq!(i1, 0);

// 使用两种方式修复下面的错误
// 1. 哪个类型实现 From 特征 : impl From<char> for ? , 我们可以查看一下之前提到的文档,来找到合适的类型
// 2. 上一章节中介绍过的某个关键字
let i3: i32 = 'a'.into();

// 使用两种方法来解决错误
let s: String = 'a' as String;

println!("Success!")
}

//第一种方法
fn main() {
// impl From<bool> for i32
let i1:i32 = false.into();
let i2:i32 = i32::from(false);
assert_eq!(i1, i2);
assert_eq!(i1, 0);

let i3:u32 = 'a'.into();

let s: String = 'a'.into();

println!("Success!")
}

//第二种方法
fn main() {
// impl From<bool> for i32
let i1:i32 = false.into();
let i2:i32 = i32::from(false);
assert_eq!(i1, i2);
assert_eq!(i1, 0);

let i3: u32 = 'a' as u32 ;

let s: String = String::from('a');
}

为自定义类型实现 From 特征

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// From 被包含在 `std::prelude` 中,因此我们没必要手动将其引入到当前作用域来
// use std::convert::From;

#[derive(Debug)]
struct Number {
value: i32,
}

impl From<i32> for Number {
// 实现 `from` 方法
fn from(item: i32) -> Self {
Number { value: item }
}
}

// 填空
fn main() {
let num = Number::from(30);
assert_eq!(num.value, 30);

let num: Number = 30.into();
assert_eq!(num.value, 30);

println!("Success!")
}

当执行错误处理时,为我们自定义的错误类型实现 From 特征是非常有用。这样就可以通过 ? 自动将某个错误类型转换成我们自定义的错误类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
use std::fs;
use std::io;
use std::num;

enum CliError {
IoError(io::Error),
ParseError(num::ParseIntError),
}

impl From<io::Error> for CliError {
fn from(error: io::Error) -> Self {
CliError::IoError(error)
}
}

impl From<num::ParseIntError> for CliError {
fn from(error: num::ParseIntError) -> Self {
CliError::ParseError(error)
}
}

fn open_and_parse_file(file_name: &str) -> Result<i32, CliError> {
// ? automatically converts io::Error to CliError
let contents = fs::read_to_string(&file_name)?;
// num::ParseIntError -> CliError
let num: i32 = contents.trim().parse()?;
Ok(num)
}

fn main() {
println!("Success!")
}

TryFrom / TryInto

类似于 From 和 Into, TryFrom 和 TryInto 也是用于类型转换的泛型特征。

但是又与 From/Into 不同, TryFrom 和 TryInto 可以对转换后的失败进行处理,然后返回一个 Result

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
let n: i16 = 256;

// Into 特征拥有一个方法`into`,
// 因此 TryInto 有一个方法是 ?
let n: u8 = match n.try_into() {
Ok(n) => n,
Err(e) => {
println!("there is an error when converting: {:?}, but we catch it", e.to_string());
0
}
};

assert_eq!(n, 0);

println!("Success!")
}

自定义实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#[derive(Debug, PartialEq)]
struct EvenNum(i32);

impl TryFrom<i32> for EvenNum {
type Error = ();

// 实现 `try_from`
fn try_from(value: i32) -> Result<Self, Self::Error> {
if value % 2 == 0 {
Ok(EvenNum(value))
} else {
Err(())
}
}
}

fn main() {
assert_eq!(EvenNum::try_from(8), Ok(EvenNum(8)));
assert_eq!(EvenNum::try_from(5), Err(()));

// 填空
let result: Result<EvenNum, ()> = 8i32.try_into();
assert_eq!(result, Ok(EvenNum(8)));
let result: Result<EvenNum, ()> = 5i32.try_into();
assert_eq!(result,Err(()));

println!("Success!")
}

其它转换

将任何类型转换成 String

只要为一个类型实现了 ToString,就可以将任何类型转换成 String。事实上,这种方式并不是最好的,可以利用 fmt::Display trait?它可以控制一个类型如何打印,在实现它的时候还会自动实现 ToString。因为 to_string 是基于 fmt::Display 实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use std::fmt;

struct Point {
x: i32,
y: i32,
}

impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "The point is ({}, {})", self.x, self.y)
}
}

fn main() {
let origin = Point { x: 0, y: 0 };
assert_eq!(origin.to_string(), "The point is (0, 0)");
assert_eq!(format!("{}", origin), "The point is (0, 0)");

println!("Success!")
}

解析 String

使用 parse 方法可以将一个 String 转换成 i32 数字,这是因为在标准库中为 i32 类型实现了 FromStr: : impl FromStr for i32

1
2
3
4
5
6
7
8
9
10
11
// To use `from_str` method, you needs to introduce this trait into the current scope.
use std::str::FromStr;
fn main() {
let parsed: i32 = "5".parse().unwrap();
let turbo_parsed = "10".parse::<i32>().unwrap();
let from_str = i32::from_str("20").unwrap();
let sum = parsed + turbo_parsed + from_str;
assert_eq!(sum, 35);

println!("Success!")
}

自定义实现 FromStr 特征

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use std::str::FromStr;
use std::num::ParseIntError;

#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32
}

impl FromStr for Point {
// 关联类型: 在一个 trait 里定义的、与这个 trait“绑定”的类型参数。
type Err = ParseIntError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let coords: Vec<&str> = s.trim_matches(|p| p == '(' || p == ')' )
.split(',')
.collect();

let x_fromstr = coords[0].parse::<i32>()?;
let y_fromstr = coords[1].parse::<i32>()?;

Ok(Point { x: x_fromstr, y: y_fromstr })
}
}
fn main() {
let p = "(3,4)".parse::<Point>();
assert_eq!(p.unwrap(), Point{ x: 3, y: 4} )
}

transmute

std::mem::transmute 是一个 unsafe 函数,可以把一个类型按位解释为另一个类型,其中这两个类型必须有同样的位数( bits )

transmute 相当于将一个类型按位移动到另一个类型,它会将源值的所有位拷贝到目标值中,然后遗忘源值。该函数跟 C 语言中的 memcpy 函数类似。

正因为此,transmute 非常非常不安全! 调用者必须要自己保证代码的安全性,当然这也是 unsafe 的目的。

示例

1.transmute 可以将一个指针转换成一个函数指针,该转换并不具备可移植性,原因是在不同机器上,函数指针和数据指针可能有不同的位数( size )。

1
2
3
4
5
6
7
8
9
10
11
fn foo() -> i32 {
0
}

fn main() {
let pointer = foo as *const ();
let function = unsafe {
std::mem::transmute::<*const (), fn() -> i32>(pointer)
};
assert_eq!(function(), 0);
}

2.transmute 还可以扩展或缩短一个不变量的生命周期,即生命周期的“非法转换!

1
2
3
4
5
6
7
8
9
10
11
// R 是一个包装了引用的结构体。它里面不存放 i32 本身,而是存放对 i32 的引用
struct R<'a>(&'a i32);

unsafe fn extend_lifetime<'b>(r: R<'b>) -> R<'static> {
std::mem::transmute::<R<'b>, R<'static>>(r)
}

unsafe fn shorten_invariant_lifetime<'b, 'c>(r: &'b mut R<'static>)
-> &'b mut R<'c> {
std::mem::transmute::<&'b mut R<'static>, &'b mut R<'c>>(r)
}

3.事实上我们还可以使用一些安全的方法来替代 transmute.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
fn main() {
/*Turning raw bytes(&[u8]) to u32, f64, etc.: */
let raw_bytes = [0x78, 0x56, 0x34, 0x12];

let num = unsafe { std::mem::transmute::<[u8; 4], u32>(raw_bytes) };

// use `u32::from_ne_bytes` instead
// 按本机的端序
let num = u32::from_ne_bytes(raw_bytes);
// or use `u32::from_le_bytes` or `u32::from_be_bytes` to specify the endianness
// 小端
let num = u32::from_le_bytes(raw_bytes);
assert_eq!(num, 0x12345678);
// 大端
let num = u32::from_be_bytes(raw_bytes);
assert_eq!(num, 0x78563412);

/*Turning a pointer into a usize: */
let ptr = &0;
let ptr_num_transmute = unsafe { std::mem::transmute::<&i32, usize>(ptr) };

// Use an `as` cast instead
let ptr_num_cast = ptr as *const i32 as usize;

/*Turning an &mut T into an &mut U: */
let ptr = &mut 0;
let val_transmuted = unsafe { std::mem::transmute::<&mut i32, &mut u32>(ptr) };


// ptr as *mut i32:把 &mut i32 转成原始指针 *mut i32。
// as *mut u32:把原始指针再转换成 *mut u32。
// &mut *(...):再把原始指针转换回可变引用。

let val_casts = unsafe { &mut *(ptr as *mut i32 as *mut u32) };

/*Turning an &str into a &[u8]: */
// this is not a good way to do this.
let slice = unsafe { std::mem::transmute::<&str, &[u8]>("Rust") };
assert_eq!(slice, &[82, 117, 115, 116]);

// You could use `str::as_bytes`
let slice = "Rust".as_bytes();
assert_eq!(slice, &[82, 117, 115, 116]);

// Or, just use a byte string, if you have control over the string
// literal
assert_eq!(b"Rust", &[82, 117, 115, 116]);
}

函数

Rust 代码中的函数和变量名使用 snake case 规范风格。在 snake case 中,所有字母都是小写并使用下划线分隔单词

我们在 Rust 中通过输入 fn 后面跟着函数名和一对圆括号来定义函数。大括号告诉编译器哪里是函数体的开始和结尾。

参数

我们可以定义为拥有 参数parameters)的函数,参数是特殊变量,是函数签名的一部分。当函数拥有参数(形参)时,可以为这些参数提供具体的值(实参)。技术上讲,这些具体值被称为参数(arguments

1
2
3
4
5
6
7
fn main() {
another_function(5);
}

fn another_function(x: i32) {
println!("The value of x is: {x}");
}

当定义多个参数时,使用逗号分隔

语句与表达式

函数体由一系列的语句和一个可选的结尾表达式构成。

语句Statements是执行一些操作但不返回值的指令

表达式Expressions计算并产生一个值

语句不返回值,表达式会计算出一个值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
let x = 5u32;

let y = {
let x_squared = x * x;
let x_cube = x_squared * x;

// 下面表达式的值将被赋给 `y`
x_cube + x_squared + x
};

let z = {
// 分号让表达式变成了语句,因此返回的不再是表达式 `2 * x` 的值,而是语句的值 `()`
2 * x;
};

println!("x is {:?}", x);
println!("y is {:?}", y);
println!("z is {:?}", z);
}

语句 let y = 6; 中的 6 是一个表达式,它计算出的值是 6。函数调用是一个表达式宏调用是一个表达式用大括号创建的一个新的块作用域也是一个表达式,例如:

1
2
3
4
5
6
7
8
fn main() {
let y = {
let x = 3;
x + 1
};

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

这个表达式:

1
2
3
4
{
let x = 3;
x + 1
}

是一个代码块,它的值是 4。表达式的结尾没有分号。如果在表达式的结尾加上分号,它就变成了语句,而语句不会返回值。

返回值

不对返回值命名,但要在箭头(->)后声明它的类型

在 Rust 中,函数的返回值等同于函数体最后一个表达式的值。使用 return 关键字和指定值,可从函数中提前返回;但大部分函数隐式的返回最后的表达式

1
2
3
4
5
6
7
8
9
fn main() {
let x = plus_one(5);

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

fn plus_one(x: i32) -> i32 {
x + 1
}

返回类型为()

1
2
3
4
5
6
7
8
9
fn main(){
println!("{}",type_of(&println!("helloworld")))
}
fn type_of<T>(_: &T) -> String{
format!("{}",std::any::type_name::<T>())
}
//output:
//helloworld
//()

返回类型为 never

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use std::thread;
use std::time;

fn never_return() -> ! {
// implement this function, don't modify fn signatures
loop {
println!("I return nothing");
// sleeping for 1 second to avoid exhausting the cpu resource
thread::sleep(time::Duration::from_secs(1))
}
}

fn main() {
never_return();
}

发散函数(Diverging function)

发散函数( Diverging function )不会返回任何值,因此它们可以用于替代需要返回任何值的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
fn main() {
println!("Success!");
}

fn get_option(tp: u8) -> Option<i32> {
match tp {
1 => {
// TODO
}
_ => {
// TODO
}
};

// 这里与其返回一个 None,不如使用发散函数替代
never_return_fn()
}

// 使用三种方法实现以下发散函数
fn never_return_fn() -> ! {

}
fn main() {
println!("Success!");
}

fn get_option(tp: u8) -> Option<i32> {
match tp {
1 => {
// TODO
}
_ => {
// TODO
}
};

never_return_fn()
}

// IMPLEMENT this function
// DON'T change any code else
fn never_return_fn() -> ! {
unimplemented!()
}
// IMPLEMENT this function in THREE ways
fn never_return_fn() -> ! {
panic!()
}

// IMPLEMENT this function in THREE ways
fn never_return_fn() -> ! {
todo!();
}
// IMPLEMENT this function in THREE ways
fn never_return_fn() -> ! {
loop {
std::thread::sleep(std::time::Duration::from_secs(1))
}
}

The difference between unimplemented! and [todo] is that while todo! conveys an intent of implementing the functionality later and the message is “not yet implemented”, unimplemented! makes no such claims. Its message is “not implemented”. Also some IDEs will mark todo!s.

调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#[allow(unused)]

fn main() {
get_option(3);
println!("Success!");
}

fn get_option(tp: u8) -> Option<i32> {
match tp {
1 => {
// TODO
}
_ => {
// TODO
}
};

never_return_fn()
}

// IMPLEMENT this function
// DON'T change any code else
fn never_return_fn() -> ! {
loop {
std::thread::sleep(std::time::Duration::from_secs(1))
}
}

使用 unimplemented!()和 todo!();会报以下错误

thread ‘main’ panicked at ‘not implemented’, src\main.rs:24:5

1
2
3
4
5
6
7
8
let _v = match b {
true => 1,
// 发散函数也可以用于 `match` 表达式,用于替代任何类型的值
false => {
println!("Success!");
panic!("we have no value for `false`, but we can panic")
}
};

控制流

if 表达式

1
2
3
4
5
6
7
8
9
fn main() {
let number = 3;

if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}

if 表达式中与条件关联的代码块有时被叫做 *arms

if/else 可以用作表达式来进行赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
let n = 5;

let big_n =
if n < 10 && n > -10 {
println!(" 数字太小,先增加 10 倍再说");

10 * n
} else {
println!("数字太大,我们得让它减半");

n / 2
};

println!("{} -> {}", n, big_n);
}

注意

1.代码中的条件必须是 bool 值。如果条件不是 bool 值,我们将得到一个错误。Rust 并不会尝试自动地将非布尔值转换为布尔值。必须总是显式地使用布尔值作为 if 的条件。

2.如果使用了多于 1 个 else if 最好使用 match 对代码进行重构

在 let 语句中使用 if

因为 if 是一个表达式,我们可以在 let 语句的右侧使用它

1
2
3
4
5
6
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };

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

if 的每个分支的可能的返回值都必须是相同类型

注意

if 代码块中的表达式返回一个整数,而 else 代码块中的表达式返回一个字符串。这不可行,因为变量必须只有一个类型。Rust 需要在编译时就确切的知道变量的类型

循环

loop

1
2
3
4
5
fn main() {
loop {
println!("again!");
}
}
从循环中返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let mut counter = 0;

let result = loop {
counter += 1;

if counter == 10 {
break counter * 2;
}
};

println!("The result is {result}");
}
循环标签

如果存在嵌套循环,break 和 continue 应用于此时最内层的循环。你可以选择在一个循环上指定一个 循环标签loop label),然后将标签与 break 或 continue 一起使用,使这些关键字应用于已标记的循环而不是最内层的循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
let mut count = 0;
'counting_up: loop {
println!("count = {count}");
let mut remaining = 10;

loop {
println!("remaining = {remaining}");
if remaining == 9 {
break;
}
if count == 2 {
break 'counting_up;
}
remaining -= 1;
}

count += 1;
}
println!("End count = {count}");
}

while

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let mut number = 3;

while number != 0 {
println!("{number}!");

number -= 1;
}

println!("LIFTOFF!!!");
}

for

1
2
3
4
5
6
7
fn main() {
let a = [10, 20, 30, 40, 50];

for element in a {
println!("the value is: {element}");
}
}

for 循环遍历集合元素相较于 while 循环,增强了代码安全性,并消除了可能由于超出数组的结尾或遍历长度不够而缺少一些元素而导致的 bug

对于没有实现 copy 的可迭代对象 for in 会取得所有权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
let names = [String::from("liming"),String::from("hanmeimei")];
for name in &names {
// do something with name...
}

println!("{:?}", names);

let numbers = [1, 2, 3];
// numbers中的元素实现了 Copy,因此无需转移所有权
for n in numbers {
// do something with name...
}

println!("{:?}", numbers);
}

通过索引和值的方式迭代数组

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

// 通过索引和值的方式迭代数组 `a`
for (i,v) in a.iter().enumerate() {
println!("第{}个元素是{}",i+1,v);
}
}

Range

它是标准库提供的类型,用来生成从一个数字开始到另一个数字之前结束的所有数字的序列。(不包括结束的数字)

rev 方法可以反转 range

1
2
3
4
5
6
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}

所有权,引用与借用

栈(Stack)与堆(Heap)

栈和堆都是代码在运行时可供使用的内存,但是它们的结构不同。栈以放入值的顺序存储值并以相反顺序取出值。这也被称作 后进先出last in, first out)。

增加数据叫做 进栈pushing onto the stack),而移出数据叫做 出栈popping off the stack)。栈中的所有数据都必须占用已知且固定的大小。

在编译时大小未知或大小可能变化的数据,要存储在堆上。 堆是缺乏组织的:当向堆放入数据时,你要请求一定大小的空间。内存分配器(memory allocator)在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 指针pointer)。这个过程称作 在堆上分配内存allocating on the heap),有时简称为 “分配”(allocating)。(将数据推入栈中并不被认为是分配)。因为指向放入堆中数据的指针是已知的并且大小是固定的,你可以将该指针存储在栈上,不过当需要实际数据时,必须访问指针。

入栈比在堆上分配内存要快,因为(入栈时)分配器无需为存储新数据去搜索内存空间;其位置总是在栈顶。相比之下,在堆上分配内存则需要更多的工作,这是因为分配器必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备。

访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。现代处理器在内存中跳转越少就越快(缓存),出于同样原因,处理器在处理的数据彼此较近的时候(比如在栈上)比较远的时候(比如可能在堆上)能更好的工作。

当你的代码调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。

跟踪哪部分代码正在使用堆上的哪些数据,最大限度的减少堆上的重复数据的数量,以及清理堆上不再使用的数据确保不会耗尽空间,这些问题正是所有权系统要处理的。一旦理解了所有权,你就不需要经常考虑栈和堆了,不过明白了所有权的主要目的就是为了管理堆数据,能够帮助解释为什么所有权要以这种方式工作。

所有权规则

  1. Rust 中的每一个值都有一个 所有者(owner)
  2. 值在任一时刻有且只有一个所有者。
  3. 当所有者(变量)离开作用域,这个值将被丢弃。

所有权的例子:

修改下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let s = give_ownership();
println!("{}", s);
}

// 只能修改下面的代码!
fn give_ownership() -> String {
let s = String::from("hello, world");
// convert String to Vec
// 将 String 转换成 Vec 类型
let _s = s.into_bytes();//into_bytes会转移所有权
s
}

方法

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let s = give_ownership();
println!("{}", s);
}

// Only modify the code below!
fn give_ownership() -> String {
let s = String::from("hello, world");
// convert String to Vec
let _s = s.as_bytes();//as_bytes不会转移所有权
s
}

1
2
3
4
5
6
7
8
9
10
fn main() {
let s = give_ownership();
println!("{}", s);
}

// Only modify the code below!
fn give_ownership() -> String {
let s = String::from("hello, world");
s
}

当所有权转移时,可变性也可以随之改变。

1
2
3
4
5
6
7
fn main() {
let s = String::from("hello, ");

let mut s1 = s;

s1.push_str("world")
}

变量与作用域

作用域是一个项(item)在程序中有效的范围。假设有这样一个变量:

let s = “hello”;

变量 s 绑定到了一个字符串字面值,这个字符串值是硬编码进程序代码中的。这个变量从声明的点开始直到当前 作用域 结束时都是有效的。示例 4-1 中的注释标明了变量 s 在何处是有效的。

1
2
3
4
5
{                      // s 在这里无效, 它尚未声明
let s = "hello"; // 从此处起,s 是有效的

// 使用 s
} // 此作用域已结束,s 不再有效
  • 当 s进入作用域 时,它就是有效的。
  • 这一直持续到它 离开作用域 为止。

str 和&str

正常情况下我们无法使用 str 类型,但是可以使用 &str 来替代

1
2
3
fn main() {
let s: &str = "hello, world";
}

如果要使用 str 类型,只能配合 Box。

1
2
3
4
5
6
7
8
fn main() {
let s: Box<str> = "hello, world".into();
greetings(s)
}

fn greetings(s: Box<str>) {
println!("{}",s)
}

& 可以用来将 Box 转换为 &str 类型 , Rust 的 Deref coercion 会把 &Box<str> 自动转换为 &str

1
2
3
4
5
6
7
8
fn main() {
let s: Box<str> = "hello, world".into();
greetings(&s)
}

fn greetings(s: &str) {
println!("{}",s)
}

String 类型

String 是定义在标准库中的类型,分配在堆上,可以动态的增长。它的底层存储是动态字节数组的方式( Vec ),但是与字节数组不同,String 是 UTF-8 编码

Unicode 和编码方式的区别

  • Unicode
    • 是一个字符集(character set),规定了每个字符对应一个唯一的 码点(code point)
    • 码点形式:U+0000 ~ U+10FFFF
    • 例如:
      • U+4F60 → ‘你’
      • U+1F600 → 😀
  • UTF-8 / UTF-16 / UTF-32
    • 是把 Unicode 码点转换成 字节序列 的具体方法
    • 也就是说,Unicode 是“字符表”,UTF-8 是“如何存储或传输这些字符的编码规则”

String.chars()和 String.bytes()分别以 Unicode 字符和字节遍历。

  • 一个 Unicode 字符的长度不是固定的
  • 一个 Unicode 字符并不一定是一个完整显示的字符

在 Unicode 和文本处理里,字符簇(grapheme cluster) 是一个用户感知的“字符单位”,也就是说,它是用户看到的一个完整字符,但它可能由 多个 Unicode 标量值(char)组成

简单来说:

  • 一个字符簇 ≈ “一个完整显示字符”
  • 不同于 Rust 的 charchar 是单个 Unicode 标量值(可能是一个字母、一个汉字、或一个 emoji 的组成部分)
  • 一个字符簇可能包含:
    • 基础字符 + 组合符号(比如重音符)
    • emoji 组合(如 👨‍👩‍👧‍👦 家庭表情,由多个 emoji 和零宽连接符组成)

要遍历字符簇,需要第三方包,比如:

String 管理被分配到堆上的数据,所以能够存储在编译时未知大小的文本。可以使用 from 函数基于字符串字面值来创建 String

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

可以 修改此类字符串 :

1
2
3
4
5
let mut s = String::from("hello");

s.push_str(", world!"); // push_str() 在字符串后追加字面值

println!("{}", s); // 将打印 `hello, world!`

内存与分配

字符串字面值来说,我们在编译时就知道其内容,所以文本被直接硬编码进最终的可执行文件中。这使得字符串字面值快速且高效。不过这些特性都只得益于字符串字面值的不可变性。不幸的是,我们不能为了每一个在编译时大小未知的文本而将一块内存放入二进制文件中,并且它的大小还可能随着程序运行而改变。

对于 String 类型,为了支持一个可变,可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容。这意味着:

  • 必须在运行时向内存分配器(memory allocator)请求内存。
  • 需要一个当我们处理完 String 时将内存返回给分配器的方法。(某些语言的垃圾回收 GC)

Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。下面是示例 4-1 中作用域例子的一个使用 String 而不是字符串字面值的版本:

1
2
3
4
5
6
{
let s = String::from("hello"); // 从此处起,s 是有效的

// 使用 s
} // 此作用域已结束,
// s 不再有效

这是一个将 String 需要的内存返回给分配器的很自然的位置:当 s 离开作用域的时候。当变量离开作用域,Rust 为我们调用一个特殊的函数。这个函数叫做 drop,在这里 String 的作者可以放置释放内存的代码。Rust 在结尾的 } 处自动调用 drop。

变量与数据交互的方式

move

栈数据

1
2
let x = 5;
let y = x;

将 5 绑定到 x;接着生成一个值 x 的拷贝并绑定到 y”。现在有了两个变量,x 和 y,都等于 5

因为整数是有已知固定大小的简单值,所以这两个 5 被放入了栈中。

对于此类数据,移动和克隆没有区别

1
2
let s1 = String::from("hello");
let s2 = s1;

一个 String 由 3 部分组成:

  1. 一个指向存放字符串内容的内存的指针
  2. 一个长度 len,指存放字符串内容所需的字节数
  3. 一个容量 capacity,指 String 从操作系统中总共获得内存的总字节数

上面这些存放在栈上,存放字符串内容的部分存放在堆上

当我们将 s1 赋值给 s2,如果 String 的数据被复制了,这意味着我们从栈上拷贝了它的指针、长度和容量。我们并没有复制指针指向的堆上数据。

当变量离开时,会调用 drop,导致 double free

为了保证内存安全

  • Rust 没有尝试复制被分配的内存
  • Rust 让 s1 失效,即变量 s1 离开作用域时不需要释放任何东西(对应所有权规则 2:值在任一时刻有且只有一个所有者)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);
warning: unused variable: `s2`
--> src\main.rs:3:9
|
3 | let s2 = s1;
| ^^ help: if this is intentional, prefix it with an underscore: `_s2`
|
= note: `#[warn(unused_variables)]` on by default

error[E0382]: borrow of moved value: `s1`
--> src\main.rs:5:28
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{}, world!", s1);
| ^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0382`.
warning: `loop_test` (bin "loop_test") generated 1 warning
error: could not compile `loop_test` due to previous error; 1 warning emitted

rust 的这种方式不同于浅拷贝,因为在浅拷贝的同时让被拷贝者失效了,因此使用新的术语:移动(Move)

隐含的设计原则 : rust 不会自动创建数据的深拷贝

因为就运行性能而言,任何自动赋值的操作都是廉价的。

部分 move

当解构一个变量时,可以同时使用 move 和引用模式绑定的方式。当这么做时,部分 move 就会发生:变量中一部分的所有权被转移给其它变量,而另一部分我们获取了它的引用。

在这种情况下,原变量将无法再被使用,但是它没有转移所有权的那一部分依然可以使用,也就是之前被引用的那部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
fn main() {
#[derive(Debug)]
struct Person {
name: String,
age: Box<u8>,
}

let person = Person {
name: String::from("Alice"),
age: Box::new(20),
};

// 通过这种解构式模式匹配,person.name 的所有权被转移给新的变量 `name`
// 但是,这里 `age` 变量却是对 person.age 的引用, 这里 ref 的使用相当于: let age = &person.age
let Person { name, ref age } = person;

println!("The person's age is {}", age);

println!("The person's name is {}", name);

// Error! 原因是 person 的一部分已经被转移了所有权,因此我们无法再使用它
//println!("The person struct is {:?}", person);

// 虽然 `person` 作为一个整体无法再被使用,但是 `person.age` 依然可以使用
println!("The person's age from person struct is {}", person.age);
}

clone

如果相对 heap 的数据进行深拷贝,而不仅仅时 stack 上面的数据,可以使用 clone 方法

1
2
3
4
5
6
7
8
9
10
11
12
let s1=String::from("Hello");
let s2=s1.clone();
println!("{},{}",s1,s2);
#[allow(unused)]

fn main() {
let t = (String::from("hello"), String::from("world"));

let (s1, s2) = t.clone();

println!("{:?}, {:?}, {:?}", s1, s2, t); // -> "hello", "world", ("hello", "world")
}

copy

Copy trait,可以用于像整数这样完全放在 stack 上面的类型

  • 如果一个类型实现了 Copy trait,那么旧的变量在赋值后仍然可用
  • 如果一个类型或该类型的一部分实现了 Drop trait,那么 Rust 不允许让它再去实现 Copy trait 了

任何简单标量及其组合类型都是 Copy 的

任何需要分配内存或某种资源的都不是 Copy 的

一些拥有 Copy trait 的类型:

  • 所有整数类型,比如 u32。
  • 布尔类型,bool,它的值是 true 和 false。
  • 所有浮点数类型,比如 f64。
  • 字符类型,char。
  • 元组,当且仅当其包含的类型也都实现 Copy 的时候。比如,(i32, i32) 实现了 Copy,但 (i32, String) 就没有。

所有权与函数

将值传递给函数与给变量赋值的原理相似。向函数传递值可能会移动或者复制,就像赋值语句一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn main() {
let s = String::from("hello"); // s 进入作用域

takes_ownership(s); // s 的值移动到函数里 ...
// ... 所以到这里不再有效

let x = 5; // x 进入作用域

makes_copy(x); // x 应该移动函数里,
// 但 i32 是 Copy 的,
// 所以在后面可继续使用 x

} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
// 没有特殊之处

fn takes_ownership(some_string: String) { // some_string 进入作用域
println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。
// 占用的内存被释放

fn makes_copy(some_integer: i32) { // some_integer 进入作用域
println!("{}", some_integer);
} // 这里,some_integer 移出作用域。没有特殊之处

当尝试在调用 takes_ownership 后使用 s 时,Rust 会抛出一个编译时错误。

返回值与作用域

返回值也可以转移所有权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
fn main() {
let s1 = gives_ownership(); // gives_ownership 将返回值
// 转移给 s1

let s2 = String::from("hello"); // s2 进入作用域

let s3 = takes_and_gives_back(s2); // s2 被移动到
// takes_and_gives_back 中,
// 它也将返回值移给 s3
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
// 所以什么也不会发生。s1 离开作用域并被丢弃

fn gives_ownership() -> String { // gives_ownership 会将
// 返回值移动给
// 调用它的函数

let some_string = String::from("yours"); // some_string 进入作用域.

some_string // 返回 some_string
// 并移出给调用的函数
//
}

// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
//

a_string // 返回 a_string 并移出给调用的函数
}

变量的所有权总是遵循相同的模式:

  • 将值赋给另一个变量时移动它
  • 当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。

如果让函数获得所得值而不获得所有权,需要把传入的参数返回

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let s1 = String::from("hello");

let (s2, len) = calculate_length(s1);

println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() 返回字符串的长度

(s, length)
}

这样过于麻烦,Rust 对此提供了一个不用获取所有权就可以使用值的功能,叫做 引用references)。

引用与借用

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let s1 = String::from("hello");

let len = calculate_length(&s1);

println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
s.len()
}

引用reference)像一个指针,因为它是一个地址,我们可以由此访问储存于该地址的属于其他变量的数据。

与指针不同,引用确保指向某个特定类型的有效值。

1
2
3
4
5
6
7
fn main() {
let x = 5;
// 填写空白处
let p = &x;

println!("x 的内存地址是 {:p}", p); // output: 0x16fa3ac84
}

注意:与使用 & 引用相反的操作是 解引用dereferencing),它使用解引用运算符 “ * “

1
2
3
let s1 = String::from("hello");

let len = calculate_length(&s1);

&s1 语法让我们创建一个 指向 值 s1 的引用,但是并不拥有它。因为并不拥有这个值,所以当引用停止使用时,它所指向的值也不会被丢弃

我们将创建一个引用的行为称为 借用borrowing

正如变量默认是不可变的,引用也一样。引用(默认)不允许修改引用的值。

rust 会在某些情况下自动解引用

1
2
3
4
5
6
7
fn main() {
let mut s = String::from("hello, ");

let p = &mut s;

p.push_str("world");
}

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
let mut s = String::from("hello, ");

borrow_object(&s)
}

fn borrow_object(s: &String) {}
fn main() {
let mut s = String::from("hello, ");

push_str(&mut s)
}

fn push_str(s: &mut String) {
s.push_str("world")
}

可变引用

1
2
3
4
5
6
7
8
9
fn main() {
let mut s = String::from("hello");

change(&mut s);
}

fn change(some_string: &mut String) {
some_string.push_str(", world");
}

可变引用有一个很大的限制:某一时刻只能存在一个可变引用

1
2
3
4
5
6
let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);

这个限制的好处是 Rust 可以在编译时就避免数据竞争。数据竞争data race)类似于竞态条件,它可由这三个行为造成:

  • 两个或更多指针同时访问同一数据。
  • 至少有一个指针被用来写入数据。
  • 没有同步数据访问的机制。

可以使用大括号来创建一个新的作用域,以允许拥有多个可变引用,只是不能 同时 拥有:

1
2
3
4
5
6
7
let mut s = String::from("hello");

{
let r1 = &mut s;
} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用

let r2 = &mut s;

另外一个限制:不可以同时拥有一个可变引用和一个不可变的引用

但是多个不可变的引用是可以的

1
2
3
4
5
6
7
let mut s = String::from("hello");

let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题

println!("{}, {}, and {}", r1, r2, r3);

悬空引用(悬垂引用 Dangling References)

在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。

让我们尝试创建一个悬垂引用,Rust 会通过一个编译时错误来避免:

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn main() {
let reference_to_nothing = dangle();
}

fn dangle() -> &String {
let s = String::from("hello");//s在函数结束后就drop了
&s//返回引用,但是函数结束后该地址就被释放掉了
}
Compiling loop_test v0.1.0 (C:\Users\cauchy\Desktop\rust\loop_test)
error[E0106]: missing lifetime specifier
--> src\main.rs:5:16
|
5 | fn dangle() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
|
5 | fn dangle() -> &'static String {
| +++++++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `loop_test` due to previous error

ref

ref 与 & 类似,可以用来获取一个值的引用,但是它们的用法有所不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
let c = '中';

let r1 = &c;

let ref r2 = c;

assert_eq!(*r1, *r2);

// 判断两个内存地址的字符串是否相等
assert_eq!(get_addr(r1),get_addr(r2));
}

// 获取传入引用的内存地址的字符串形式
fn get_addr(r: &char) -> String {
format!("{:p}", r)
}

引用规则(借用规则)总结

  • 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
  • 引用必须总是有效的。

Ok: 从可变对象借用不可变

1
2
3
4
5
6
7
8
9
fn main() {
let mut s = String::from("hello, ");

borrow_object(&s);

s.push_str("world");
}

fn borrow_object(s: &String) {}

None Lexical Lifetimes(NLL)

非词法作用域生命周期

例子

1
2
3
4
5
6
7
8
9
10
11
// 注释掉一行代码让它工作
fn main() {
let mut s = String::from("hello, ");

let r1 = &mut s;
r1.push_str("world");
let r2 = &mut s;
r2.push_str("!");

println!("{}",r1);
}

注释掉 println 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
let mut s = String::from("hello, ");

let r1 = &mut s;
r1.push_str("world");//rust编译器知道这之后r1对s的借用生命周期结束了
let r2 = &mut s;
r2.push_str("!");

//println!("{}",r1);
}
fn main() {
let mut x = 22;

let p = &mut x; // mutable borrow

println!("{}", x); // later used
}

这段代码顺利编译,因为编译器知道 x 的可变借用并没有持续到作用域结尾,而是在 x 被再次使用之前就结束了,所以这里不存在冲突。

1
2
3
4
5
6
7
8
9
10
fn main() {
let mut s = String::from("hello, ");

let r1 = &mut s;
let r2 = &mut s;

// 在下面增加一行代码人为制造编译错误:cannot borrow `s` as mutable more than once at a time
// 你不能同时使用 r1 和 r2

}

加入 r1.push_str(“world”);即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
warning: unused variable: `r2`
--> src/main.rs:5:9
|
5 | let r2 = &mut s;
| ^^ help: if this is intentional, prefix it with an underscore: `_r2`
|
= note: `#[warn(unused_variables)]` on by default

error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src/main.rs:5:14
|
4 | let r1 = &mut s;
| ------ first mutable borrow occurs here
5 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
...
9 | r1.push_str("world");
| -- first borrow later used here

For more information about this error, try `rustc --explain E0499`.
warning: `rust_programming` (bin "rust_programming") generated 1 warning
error: could not compile `rust_programming` (bin "rust_programming") due to 1 previous error; 1 warning emitted

切片类型 Slice

slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。slice 是一类引用,所以它没有所有权。

1
2
3
4
5
6
7
8
9
10
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();

for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}

该函数接收一个用空格分隔单词的字符串,并返回在该字符串中找到的第一个单词。如果函数在该字符串中并未找到空格,则整个字符串就是一个单词,所以应该返回整个字符串。

first_word 函数有一个参数 &String。因为我们不需要所有权,所以这没有问题。不过应该返回什么呢?我们并没有一个真正获取 部分 字符串的办法。不过,我们可以返回单词结尾的索引,结尾由一个空格表示

因为需要逐个元素的检查 String 中的值是否为空格,需要用 as_bytes 方法将 String 转化为字节数组:

let bytes = s.as_bytes();

接下来,使用 iter 方法在字节数组上创建一个迭代器:

for (i, &item) in bytes.iter().enumerate() {

因为 enumerate 方法返回一个元组,我们可以使用模式来解构,所以在 for 循环中,我们指定了一个模式,其中元组中的 i 是索引而元组中的 &item 是单个字节。因为我们从 .iter().enumerate() 中获取了集合元素的引用,所以模式中使用了 &。

不过这有一个问题。我们返回了一个独立的 usize,不过它只在 &String 的上下文中才是一个有意义的数字。换句话说,因为它是一个与 String 相分离的值,无法保证将来它仍然有效。

字符串切片 string slice

字符串 slicestring slice)是 String 中一部分值的引用,它看起来像这样:

1
2
3
4
let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

[开始索引..终止索引]

[starting_index..ending_index]

其中 starting_index 是 slice 的第一个位置,ending_index 则是 slice 最后一个位置的后一个值。

如果想要从索引 0 开始,可以不写两个点号之前的值

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

let slice = &s[0..2];
let slice = &s[..2];

如果 slice 包含 String 的最后一个字节,也可以舍弃尾部的数字

1
2
3
4
5
6
let s = String::from("hello");

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];

也可以同时舍弃这两个值来获取整个字符串的 slice

1
2
3
4
5
6
let s = String::from("hello");

let len = s.len();

let slice = &s[0..len];
let slice = &s[..];

注意字符串 slice range 的索引必须位于有效的 UTF-8 字符边界内,如果尝试从一个多字节字符的中间位置创建字符串 slice,则程序将会因错误而退出。

1
2
3
4
5
6
fn main() {
let s = "你好,世界";
let slice = &s[0..2];
println!("{}",slice);
assert!(slice == "你");
}
1
2
3
thread 'main' panicked at src/main.rs:3:19:
byte index 2 is not a char boundary; it is inside '你' (bytes 0..3) of `你好,世界`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

rust 中 String 索引操作 s[i] 不直接返回字符,因为 UTF-8 是可变长度编码。需要使用 chars()bytes() 来遍历或操作字符串内容。比如 chars() 可以迭代 Unicode 字符,而bytes()单个字节。

重写函数,返回一个 slice(字符串切片返回值可以写成:&str)

1
2
3
4
5
6
7
8
9
10
11
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();

for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}

&s[..]
}

调用

1
2
3
4
5
6
7
8
9
fn main() {
let mut s = String::from("hello world");

let word = first_word(&s);

s.clear(); // 错误! s需要是一个可变引用

println!("the first word is: {}", word);
}

当拥有某值的不可变引用时,就不能再获取一个可变引用。因为 clear 需要清空 String,它尝试获取一个可变引用。在调用 clear 之后的 println! 使用了 word 中的引用,所以这个不可变的引用在此时必须仍然有效。Rust 不允许 clear 中的可变引用和 word 中的不可变引用同时存在,因此编译失败

字符串字面值就是 slice

1
let s = "Hello, world!";

这里 s 的类型是 &str:它是一个指向二进制程序特定位置的 slice,这也就是为什么字符串字面值是不可变的;&str 是一个不可变引用。

字符串 slice 作为参数

在知道了能够获取字面值和 String 的 slice 后,我们对 first_word 做了改进,这是它的签名:

fn first_word(s: &String) -> &str {

而更有经验的 Rustacean 会编写出如下的签名,因为它使得可以对 &String 值和 &str 值使用相同的函数:

fn first_word(s: &str) -> &str {

如果有一个字符串 slice,可以直接传递它。如果有一个 String,则可以传递整个 String 的 slice 或对 String 的引用。这种灵活性利用了 deref coercions 的优势,定义一个获取字符串 slice 而不是 String 引用的函数使得我们的 API 更加通用并且不会丢失任何功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
let my_string = String::from("hello world");

// `first_word` 适用于 `String`(的 slice),整体或全部
let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);
// `first_word` 也适用于 `String` 的引用,
// 这等价于整个 `String` 的 slice
let word = first_word(&my_string);

let my_string_literal = "hello world";

// `first_word` 适用于字符串字面值,整体或全部
let word = first_word(&my_string_literal[0..6]);
let word = first_word(&my_string_literal[..]);

// 因为字符串字面值已经是字符串 slice 了
// 这也是适用的,无需 slice 语法!
let word = first_word(my_string_literal);
}

&String 可以被隐式地转换为&str 类型

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let mut s = String::from("hello world");

// 这里, &s 是 `&String` 类型,但是 `first_character` 函数需要的是 `&str` 类型。
// 尽管两个类型不一样,但是代码仍然可以工作,原因是 `&String` 会被隐式地转换成 `&str` 类型
let ch = first_character(&s);

println!("the first character is: {}", ch);
s.clear();
}
fn first_character(s: &str) -> &str {
&s[..1]
}

其他类型的 slice

字符串 slice,是针对字符串的。不过也有更通用的 slice 类型。考虑一下这个数组:

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

就跟我们想要获取字符串的一部分那样,我们也会想要引用数组的一部分。我们可以这样做:

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

let slice = &a[1..3];

assert_eq!(slice, &[2, 3]);

这个 slice 的类型是 &[i32]。它跟字符串 slice 的工作方式一样,通过存储第一个集合元素的引用和一个集合总长度。你可以对其他所有集合使用这类 slice。

切片跟数组相似,但是切片的长度无法在编译期得知,因此你无法直接使用切片类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 修复代码中的错误,不要新增代码行!
fn main() {
let arr = [1, 2, 3];
let s1: [i32] = arr[0..2];

let s2: str = "hello, world" as str;
}
//修复后
fn main() {
let arr = [1, 2, 3];
let s1: &[i32] = &arr[0..2];

let s2: &str = "hello, world" as &str;
}
  • 一个切片引用占用了2 个字大小的内存空间( 从现在开始,为了简洁性考虑,如无特殊原因,我们统一使用切片来特指切片引用 )。 该切片的第一个字是指向数据的指针,第二个字是切片的长度
  • 字的大小取决于处理器架构,例如在 x86-64 上,字的大小是 64 位也就是 8 个字节,那么一个切片引用就是 16 个字节大小。
1
2
3
4
5
6
7
fn main() {
let arr: [char; 3] = ['中', '国', '人'];

let slice = &arr[..2];

assert!(std::mem::size_of_val(&slice) == 16);
}
  • 切片( 引用 )可以用来借用数组的某个连续的部分,对应的签名是 &[T],可以与数组的签名对比下 [T; Length]。
1
2
3
4
5
6
fn main() {
let arr: [i32; 5] = [1, 2, 3, 4, 5];

let slice: &[i32] = &arr[1..4];
assert_eq!(slice, &[2, 3, 4]);
}

结构体

定义结构体

需要使用 struct 关键字并为整个结构体提供一个名字。

在大括号中,定义每一部分数据的名字和类型,我们称为 字段field

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

实例化

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

你可以在实例化一个结构体时将它整体标记为可变的,但是 Rust 不允许我们将结构体的某个字段专门指定为可变的.

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Person {
name: String,
age: u8,
}
fn main() {
let age = 18;
let mut p = Person {
name: String::from("sunface"),
age,
};
p.age = 30;
p.name = String::from("sunfei");
}

访问

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

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

一旦 struct 的实例是可变的,那么实例中所有的字段都是可变的

字段初始化简写

当字段名与字段值对应的变量名相同时,就可以使用字段初始化简写的方式

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

Struct 更新语法

基于 现有的 struct 实例创建一个新的实例

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,
};
}

使用 struct 更新语法

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

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

Tuple Struct 元组结构体

元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。

适用于给整个元组取一个名字,并使元组成为与其他元组不同的类型时

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);
}

访问此类结构体,与访问元组相同:

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

fn main() {
let p = Point(10, 20);
println!("x = {}, y = {}", p.0, p.1);
}

没有任何字段的类单元结构体(unit-like structs)

没有任何字段的类单元结构体,它们类似于 ()

1
2
3
4
5
struct AlwaysEqual;

fn main() {
let subject = AlwaysEqual;
}

struct 中的所有权

在示例 5-1 中的 User 结构体的定义中,我们使用了自身拥有所有权的 String 类型而不是 &str 字符串 slice 类型。这是一个有意而为之的选择,因为我们想要这个结构体拥有它所有的数据为此只要整个结构体是有效的话其数据也是有效的。

可以使结构体存储被其他对象拥有的数据的引用,不过这么做的话需要用上 生命周期lifetimes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct User {
active: bool,
username: &str,
email: &str,
sign_in_count: u64,
}

fn main() {
let user1 = User {
email: "someone@example.com",
username: "someusername123",
active: true,
sign_in_count: 1,
};
}

报错:缺少生命周期标识符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
error[E0106]: missing lifetime specifier
--> src/main.rs:3:15
|
3 | username: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 ~ struct User<'a> {
2 | active: bool,
3 ~ username: &'a str,
|

error[E0106]: missing lifetime specifier
--> src/main.rs:4:12
|
4 | email: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 ~ struct User<'a> {
2 | active: bool,
3 | username: &str,
4 ~ email: &'a str,
|

For more information about this error, try `rustc --explain E0106`.
error: could not compile `rust_programming` (bin "rust_programming") due to 2 previous errors

打印 struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#[derive(Debug)]
struct Rectangle{
width: u32,
length: u32,
}
fn main() {
let rect=Rectangle{
width:30,
length:50,
};
println!("{}",area(&rect));
println!("{:#?}",rect)
}
fn area(rect: &Rectangle)->u32{
rect.width*rect.length
}

struct 的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#[derive(Debug)]
struct Rectangle{
width: u32,
length: u32,
}
impl Rectangle{
fn area(&self)->u32{
self.width*self.length
}
}
fn main() {
let rect=Rectangle{
width:30,
length:50,
};
println!("{}",rect.area());
println!("{:#?}",rect)
}
  1. 在 impl 块里面定义方法
  2. 方法的第一个参数可以是&self,也可以获得其所有权或可变借用,和其他参数一样
  3. 更良好的代码组织

方法调用的运算符

在 C/C++ 语言中,有两个不同的运算符来调用方法:. 直接在对象上调用方法,而 -> 在一个对象的指针上调用方法,这时需要先解引用(dereference)指针。换句话说,如果 object 是一个指针,那么 object->something() 就像 (*object).something() 一样。

Rust 并没有一个与 -> 等效的运算符;相反,Rust 有一个叫 自动引用和解引用automatic referencing and dereferencing)的功能。方法调用是 Rust 中少数几个拥有这种行为的地方

它是这样工作的:当使用 object.something() 调用方法时,Rust 会自动为 object 添加 &、&mut 或 * 以便使 object 与方法签名匹配。也就是说,这些代码是等价的:

1
2
p1.distance(&p2);
(&p1).distance(&p2);

这种自动引用的行为之所以有效,是因为方法有一个明确的接收者———— self 的类型。在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取(&self),做出修改(&mut self)或者是获取所有权(self)。

关联函数

1
2
3
4
5
6
7
8
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}

所有在 impl 块中定义的函数被称为 关联函数associated functions

不是方法的关联函数经常被用作返回一个结构体新实例的构造函数。这些函数的名称通常为 new ,但 new 并不是一个关键字

使用结构体名和 :: 语法来调用这个关联函数:比如 let sq = Rectangle::square(3);。这个函数位于结构体的命名空间中::: 语法用于关联函数和模块创建的命名空间

每个结构体都允许拥有多个 impl 块。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
struct Point {
x: f64,
y: f64,
}

// `Point` 的关联函数都放在下面的 `impl` 语句块中
impl Point {
// 关联函数的使用方法跟构造器非常类似
fn origin() -> Point {
Point { x: 0.0, y: 0.0 }
}

// 另外一个关联函数,有两个参数
fn new(x: f64, y: f64) -> Point {
Point { x: x, y: y }
}
}

struct Rectangle {
p1: Point,
p2: Point,
}

impl Rectangle {
// 这是一个方法
// `&self` 是 `self: &Self` 的语法糖
// `Self` 是当前调用对象的类型,对于本例来说 `Self` = `Rectangle`
fn area(&self) -> f64 {
// 使用点操作符可以访问 `self` 中的结构体字段
let Point { x: x1, y: y1 } = self.p1;
let Point { x: x2, y: y2 } = self.p2;


// `abs` 是一个 `f64` 类型的方法,会返回调用者的绝对值
((x1 - x2) * (y1 - y2)).abs()
}

fn perimeter(&self) -> f64 {
let Point { x: x1, y: y1 } = self.p1;
let Point { x: x2, y: y2 } = self.p2;

2.0 * ((x1 - x2).abs() + (y1 - y2).abs())
}

// 该方法要求调用者是可变的,`&mut self` 是 `self: &mut Self` 的语法糖
fn translate(&mut self, x: f64, y: f64) {
self.p1.x += x;
self.p2.x += x;

self.p1.y += y;
self.p2.y += y;
}
}

// `Pair` 持有两个分配在堆上的整数
struct Pair(Box<i32>, Box<i32>);

impl Pair {
// 该方法会拿走调用者的所有权
// `self` 是 `self: Self` 的语法糖
fn destroy(self) {
let Pair(first, second) = self;

println!("Destroying Pair({}, {})", first, second);

// `first` 和 `second` 在这里超出作用域并被释放
}
}

fn main() {
let rectangle = Rectangle {
// 关联函数的调用不是通过点操作符,而是使用 `::`
p1: Point::origin(),
p2: Point::new(3.0, 4.0),
};

// 方法才是通过点操作符调用
// 注意,这里的方法需要的是 `&self` 但是我们并没有使用 `(&rectangle).perimeter()` 来调用,原因在于:
// 编译器会帮我们自动取引用
// `rectangle.perimeter()` === `Rectangle::perimeter(&rectangle)`
println!("Rectangle perimeter: {}", rectangle.perimeter());
println!("Rectangle area: {}", rectangle.area());

let mut square = Rectangle {
p1: Point::origin(),
p2: Point::new(1.0, 1.0),
};


// 错误!`rectangle` 是不可变的,但是这个方法要求一个可变的对象
//rectangle.translate(1.0, 0.0);
// TODO ^ 试着反注释此行,看看会发生什么

// 可以!可变对象可以调用可变的方法
square.translate(1.0, 1.0);

let pair = Pair(Box::new(1), Box::new(2));

pair.destroy();

// Error! 上一个 `destroy` 调用拿走了 `pair` 的所有权
//pair.destroy();
// TODO ^ 试着反注释此行
}

枚举

定义枚举

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

通过在代码中定义一个 IpAddrKind 枚举来表现这个概念并列出可能的 IP 地址类型,V4 和 V6。这被称为枚举的 成员variants):

在创建枚举时,你可以使用显式的整数设定枚举成员的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
enum Number {
Zero,
One,
Two,
}

enum Number1 {
Zero = 0,
One,
Two,
}

// 错误,不能用小数
//enum Number2 {
// Zero = 0.0,
// One = 1.0,
// Two = 2.0,
//}

// C-like enum
enum Number2 {
Zero = 0,
One = 1,
Two = 2,
}

fn main() {
// 通过 `as` 可以将枚举值强转为整数类型
assert_eq!(Number::One as u8, Number1::One as u8);
assert_eq!(Number1::One as u8, Number2::One as u8);
}

枚举值

可以像这样创建 IpAddrKind 两个不同成员的实例:

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

枚举成员中的值可以使用模式匹配来获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}

fn main() {
let msg = Message::Move{x: 1, y: 1};

if let Message::Move{x:a,y: b} = msg {
// 也可以写成
// if let Message::Move{x, y} = msg {
assert_eq!(a, b);
} else {
panic!("不要让这行代码运行!");
}

}

将数据附加到枚举的变体中

使用 struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum IpAddrKind {
V4,
V6,
}

struct IpAddr {
kind: IpAddrKind,
address: String,
}

let home = IpAddr {
kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};

let loopback = IpAddr {
kind: IpAddrKind::V6,
address: String::from("::1"),
};

仅仅使用枚举并将数据直接放进每一个枚举成员而不是将枚举作为结构体的一部分。IpAddr 枚举的新定义表明了 V4 和 V6 成员都关联了 String 值:

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

let home = IpAddr::V4(String::from("127.0.0.1"));

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

我们直接将数据附加到枚举的每个成员上,这样就不需要一个额外的结构体了。

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"));

注意虽然标准库中包含一个 IpAddr 的定义,仍然可以创建和使用我们自己的定义而不会有冲突,因为我们并没有将标准库中的定义引入作用域。

枚举可以嵌入多种类型

1
2
3
4
5
6
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
  • Quit 没有关联任何数据。
  • Move 类似结构体包含命名字段。
  • Write 包含单独一个 String。
  • ChangeColor 包含三个 i32。

枚举中定义函数

结构体和枚举还有另一个相似点:就像可以使用 impl 来为结构体定义方法那样,也可以在枚举上定义方法。这是一个定义于我们 Message 枚举上的叫做 call 的方法:

1
2
3
4
5
6
7
8
impl Message {
fn call(&self) {
// 在这里定义方法体
}
}

let m = Message::Write(String::from("hello"));
m.call();

方法体使用了 self 来获取调用方法的值。这个例子中,创建了一个值为 Message::Write(String::from(“hello”)) 的变量 m,而且这就是当 m.call() 运行时 call 方法中的 self 的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#[derive(Debug)]
enum TrafficLightColor {
Red,
Yellow,
Green,
}

// implement TrafficLightColor with a method
impl TrafficLightColor {
fn color(&self) -> String {
match *self {
TrafficLightColor::Red => "red".to_string(),
TrafficLightColor::Yellow => "yellow".to_string(),
TrafficLightColor::Green => "green".to_string(),
}
}
}

fn main() {
let c = TrafficLightColor::Yellow;

assert_eq!(c.color(), "yellow");

println!("{:?}", c);
}

Option 枚举

定义于标准库中,在 prelude(预导入模块中)

Rust 没有 Null,提供了类似于 Null 概念的枚举-Option,它定义于标准库中

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

使用

1
2
3
4
let some_number = Some(5);
let some_char = Some('e');

let absent_number: Option<i32> = None;

当有一个 Some 值时,我们就知道存在一个值,而这个值保存在 Some 中。当有个 None 值时,在某种意义上,它跟空值具有相同的意义:并没有一个有效的值。那么,Option 为什么就比空值要好呢?

简而言之,因为 Option 和 T(这里 T 可以是任何类型)是不同的类型,编译器不允许像一个肯定有效的值那样使用 Option。例如,这段代码不能编译,因为它尝试将 Option 与 i8 相加:

1
2
3
4
let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y;

事实上,错误信息意味着 Rust 不知道该如何将 Option 与 i8 相加,因为它们的类型不同。当在 Rust 中拥有一个像 i8 这样类型的值时,编译器确保它总是有一个有效的值。我们可以自信使用而无需做空值检查。只有当使用 Option(或者任何用到的类型)的时候需要担心可能没有值,而编译器会确保我们在使用值之前处理了为空的情况。

换句话说,在对 Option 进行 T 的运算之前必须将其转换为 T。

为了拥有一个可能为空的值,你必须要显式的将其放入对应类型的 Option 中。接着,当使用这个值时,必须明确的处理值为空的情况。只要一个值不是 Option 类型,你就 可以 安全的认定它的值不为空。

这是 Rust 的一个经过深思熟虑的设计决策,来限制空值的泛滥以增加 Rust 代码的安全性。

枚举实现链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#![allow(unused)]

enum List {
// Cons: 链表中包含有值的节点,节点是元组类型,第一个元素是节点的值,第二个元素是指向下一个节点的指针
Cons(u32, Box<List>),
// Nil: 链表中的最后一个节点,用于说明链表的结束
Nil,
}

// 为枚举实现一些方法
impl List {
// 创建空的链表
fn new() -> List {
// 因为没有节点,所以直接返回 Nil 节点
// 枚举成员 Nil 的类型是 List
Nil
}

// 在老的链表前面新增一个节点,并返回新的链表
fn prepend(self, elem: u32) -> List {
Cons(elem, Box::new(self))
}

// 返回链表的长度
fn len(&self) -> u32 {
match *self {
// 这里我们不能拿走 tail 的所有权,因此需要获取它的引用,递归计算
Cons(_,ref tail) => 1 + tail.len(),
// 空链表的长度为 0
Nil => 0
}
}

// 返回链表的字符串表现形式,用于打印输出
fn stringify(&self) -> String {
match *self {
Cons(head, ref tail) => {
// 递归生成字符串
format!("{}, {}", head, tail.stringify())
},
Nil => {
format!("Nil")
},
}
}
}

fn main() {
// 创建一个新的链表(也是空的)
let mut list = List::new();

// 添加一些元素
list = list.prepend(1);
list = list.prepend(2);
list = list.prepend(3);

// 打印列表的当前状态
println!("链表的长度是: {}", list.len());
println!("{}", list.stringify());
}

模式匹配

match 控制流结构

Rust 有一个叫做 match 的极为强大的控制流运算符,它允许我们将一个值一系列的模式相比较,并根据相匹配的模式执行相应代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}

matches!

matches!看起来像 match, 但是它可以做一些特别的事情

1
2
3
4
5
6
7
8
fn main() {
let alphabets = ['a', 'E', 'Z', '0', 'x', '9' , 'Y'];

// fill the blank with `matches!` to make the code work
for ab in alphabets {
assert!(matches!(ab, 'a'..='z' | 'A'..='Z' | '0'..='9'))
}
}

下面的代码会报错,原因是枚举默认没有实现 PartialEq,所以不能用==比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
enum MyEnum {
Foo,
Bar
}

fn main() {
let mut count = 0;

let v = vec![MyEnum::Foo,MyEnum::Bar,MyEnum::Foo];
for e in v {
if e == MyEnum::Foo { // 修复错误,只能修改本行代码
count += 1;
}
}

assert_eq!(count, 2);
}
Compiling demo v0.1.0 (C:\Users\cauchy\Desktop\rust\demo)
error[E0369]: binary operation `==` cannot be applied to type `MyEnum`
--> src\main.rs:13:14
|
13 | if e == MyEnum::Foo { // 修复错误,只能修改本行代码
| - ^^ ----------- MyEnum
| |
| MyEnum
|
note: an implementation of `PartialEq<_>` might be missing for `MyEnum`
--> src\main.rs:3:1
|
3 | enum MyEnum {
| ^^^^^^^^^^^ must implement `PartialEq<_>`
help: consider annotating `MyEnum` with `#[derive(PartialEq)]`
|
3 | #[derive(PartialEq)]
|

For more information about this error, try `rustc --explain E0369`.
error: could not compile `demo` due to previous error

修改为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum MyEnum {
Foo,
Bar
}

fn main() {
let mut count = 0;

let v = vec![MyEnum::Foo,MyEnum::Bar,MyEnum::Foo];
for e in v {
if matches!(e, MyEnum::Foo) { // 修复错误,只能修改本行代码
count += 1;
}
}

assert_eq!(count, 2);
}

绑定值的模式

匹配分支的另一个有用的功能是可以绑定匹配的模式的部分值。这也就是如何从枚举成员中提取值的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#[derive(Debug)] // 这样可以立刻看到州的名称
enum UsState {
Alabama,
Alaska,
// --snip--
}

enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
}
}
}
fn main(){
let c= Coin::Quarter(UsState::Alaska);
println!("{}",value_in_cents(c));
}

匹配 Option

1
2
3
4
5
6
7
8
9
10
11
fn main(){
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}//它获取一个 Option<i32> ,如果其中含有一个值,将其加一。如果其中没有值,函数应该返回 None 值,而不尝试执行任何操作。

match 匹配必须穷举所有的可能性

使用_占位符(必须放到最后面)

1
2
3
4
5
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(),
}

if let 简洁控制流

处理只关心一种模式匹配而忽略其它匹配的情况

1
2
3
4
5
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {}", max),
_ => (),
}

使用 if let

1
2
3
4
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {}", max);
}

在这个例子中,模式是 Some(max),max 绑定为 Some 中的值。接着可以在 if let 代码块中使用 max 了,就跟在对应的 match 分支中一样。模式不匹配时 if let 块中的代码不会执行。

放弃了穷举的可能性

可以把 if let 看作是 match 的语法糖

搭配 else 使用

1
2
3
4
5
6
let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {:?}!", state);
} else {
count += 1;
}

模式匹配中的变量遮蔽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() {
let age = Some(30);
if let Some(age) = age { // 创建一个新的变量,该变量与之前的 `age` 变量同名
assert_eq!(age, 30);
} // 新的 `age` 变量在这里超出作用域

match age {
// `match` 也能实现变量遮蔽
Some(age) => println!("age 是一个新的变量,它的值是 {}",age),
_ => ()
}
}
//output:
//age 是一个新的变量,它的值是 30

Workspace, Package,Crate,Module

  • Packages): Cargo 的一个功能,它允许你构建、测试和分享 crate。
  • Crate :一个模块的树形结构,它形成了库或二进制项目。
  • 模块Modules)和 use: 允许你控制作用域和路径的私有性。
  • 路径path):一个命名例如结构体、函数或模块等项的方式
层级 名称 说明 示例
📦 Package “包”,Cargo 管理的单位(一个项目) 包含一个或多个 crate,以及 Cargo.toml 整个项目目录
🧩 Crate “单个编译单元”,可以是库或可执行程序 每次 rustc 编译的就是一个 crate src/lib.rssrc/main.rs
📁 Module 模块,组织 crate 内部代码的结构 类似 C++ 的命名空间或 Python 的模块 mod network;
🛣️ Path 用来访问模块、结构体、函数的路径 类似 std::io::Write crate::foo::bar()

Crate 的类型

  • binary
  • library

每个 crate 是一个独立的编译单元。
Rust 编译器一次只编译一个 crate。

Crate Root

是源代码文件,Rust 编译器从这里开始,组成 Crate 的根 Module。
Crate Root 是 Rust 编译器构建 crate 时的起点文件
它决定了 模块树(module tree) 的最顶层结构。


举个例子

假设我们有一个最简单的项目:

1
2
3
4
5
my_project/
├── Cargo.toml
└── src/
├── main.rs
└── lib.rs

这两个文件都是 可能的 crate root

文件 crate 类型 作用
src/main.rs binary crate root 程序入口点(必须包含 fn main()
src/lib.rs library crate root 库的入口点(定义库的模块结构)

编译器的工作逻辑

当运行cargo build时:

Cargo 会:

  1. 读取 Cargo.toml
  2. 找到当前 package 里的 crate roots(例如 src/main.rssrc/lib.rs);
  3. 对每个 crate root 调用编译器,从这个文件开始构建整个 crate 的模块树。

一个 Package

  • 包含 1 个 Cargo.toml,它描述了如何构建这些 Crates
  • 只能包含 0-1 个 library crate
  • 可以包含任意数量的 binary crate
  • 但必须至少包含一个 crate(library 或者 binary)

举例:

1
2
3
4
5
6
7
8
my_package/
├── Cargo.toml
├── src/
│ ├── lib.rs # library crate
│ ├── main.rs # 主 binary crate
│ └── bin/
│ ├── tool1.rs # 第二个 binary crate
│ └── tool2.rs # 第三个 binary crate

在 Cargo.toml 里无需特别配置。运行:

1
2
3
4
5
6
# main.rs
cargo run
# tool1.rs
cargo run --bin tool1
# tool2.rs
cargo run --bin tool1

workspace

workspace 是多个 package 的集合,每个 package 都可以有自己的 library crate。如下例子:

1
2
3
4
5
6
7
8
9
10
11
12
my_workspace/
├── Cargo.toml # workspace 声明
├── app/ # 一个 binary crate package
│ ├── Cargo.toml
│ └── src/main.rs
├── utils/ # 一个 library crate package
│ ├── Cargo.toml
│ └── src/lib.rs
└── network/ # 另一个 library crate package
├── Cargo.toml
└── src/lib.rs

workspace 声明的 toml

1
2
[workspace]
members = ["app", "utils", "network"]

Cargo 的惯例

src/main.rs

  • binary crate 的 crate root
  • crate 名与 package 名相同

src/lib.rs

  • package 包含一个 Library crate
  • library crate 的 crate root
  • crate 名与 package 名相同

一个 package 可以同时包含 src/main.rs 和 src/lib.rs

一个 Package 可以有多个 binary crate:

文件放在 src/bin 下,每个文件都是单独的 binary crate

定义 module 来控制作用域和私有性

Module

  • 在一个 crate 内,将 crate 进行分组
  • 控制项目(item) 的私有性,public,private

建立 module

  • mod 关键字
  • 可嵌套
  • 可包含其他项的定义(struct,enum,常量,trait,函数等)的定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}

fn seat_at_table() {}
}

mod serving {
fn take_order() {}

fn serve_order() {}

fn take_payment() {}
}
}

上述代码的模块树

1
2
3
4
5
6
7
8
9
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment

src/main.rs 和 src/lib.rs 叫做 crate roots

  • 这两个文件(任意一个)的内容形成了名为 crate 的模块,位于整个模块树的根部
  • 整个模块树在隐式的 crate 模块下

路径

为了在 Rust 的模块中找到某个条目,需要使用路径

  • 绝对路径从 crate root 开始,使用 crate 名或字面值 crate
  • 相对路径从当前模块开始,使用 self,super 或当前模块的标识符

路径至少由一个标识符组成,标识符之间使用::

src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}

pub fn eat_at_restaurant() {//共有的
// 绝对路径
crate::front_of_house::hosting::add_to_waitlist();

// 相对路径
front_of_house::hosting::add_to_waitlist();
}

私有边界(privacy boundary)

  • Rust 的所有条目(函数,方法,struct,enum,模块,常量) 默认都是私有的
  • 父级模块无法访问所有子模块的私有条目
  • 子模块里可以使用所有祖先模块中的条目
  • 同级模块可以互相调用
  • pub 关键字可以标记为公共的

super

super:用来访问父级模块路径中的内容,类似于文件系统中的..

1
2
3
4
5
6
7
8
9
10
fn serve_order() {}

mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::serve_order();
}

fn cook_order() {}
}

pub struct

pub 放在 struct 前

  • struct 是公共的
  • struct 的字段默认是私有的字段前面加 pub 就可以设为公有的

文件名: src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
mod back_of_house {
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}

impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}

pub fn eat_at_restaurant() {
// 在夏天订购一个黑麦土司作为早餐
let mut meal = back_of_house::Breakfast::summer("Rye");
// 改变主意更换想要面包的类型
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);

// 如果取消下一行的注释代码不能编译;
// 不允许查看或修改早餐附带的季节水果
// meal.seasonal_fruit = String::from("blueberries");
}

pub enum

把 pub 放在 enum 前:

  • enum 是公共的
  • enum 的变体默认也都是公共的(不需要加 pub 关键字)

use 关键字

使用 use 关键字将路径引入作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}

use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
  • 仍然遵循私有性规则
  • 使用 use 来指定相对路径

use 的习惯用法

  • 函数:将函数的父级模块引入到作用域(指定到父级)

文件名: src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}

use self::front_of_house::hosting;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
  • struct,enum,其他:指定完整路径(指定到本身)

文件名: src/main.rs

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

fn main() {
let mut map = HashMap::new();
map.insert(1, 2);
}

两个具有相同名称的项带入作用域

文件名: src/lib.rs

1
2
3
4
5
6
7
8
9
10
use std::fmt;
use std::io;

fn function1() -> fmt::Result {
// --snip--
}

fn function2() -> io::Result<()> {
// --snip--
}

使用 as 关键字提供新的名称

文件名: src/lib.rs

1
2
3
4
5
6
7
8
9
10
use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
// --snip--
}

fn function2() -> IoResult<()> {
// --snip--
}

pub use 重导出

使用 use 将路径(名称)导入到作用域内后,该名称在此作用域内是私有的

如果你希望 外部模块也能通过你的路径访问 那个条目(比如函数、结构体、模块等),就可以用 pub use,re-export 重导出

pub use:重导出

  • 将条目引入到作用域
  • 该条目可以被外部代码引入到它们的作用域

pub(in Crate)

有时我们希望某一个项只对特定的包可见,那么就可以使用 pub(in Crate) 语法.

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pub mod a {
pub const I: i32 = 3;

fn semisecret(x: i32) -> i32 {
use self::b::c::J;
x + J
}

pub fn bar(z: i32) -> i32 {
semisecret(I) * z
}
pub fn foo(y: i32) -> i32 {
semisecret(I) + y
}

mod b {
pub(in crate::a) mod c {
pub(in crate::a) const J: i32 = 4;
}
}
}

使用外部包(package)

1.Cargo.toml 添加依赖的包

2.use 将特定条目引入到作用域

  • 标准库 std 也被当做外部包,但是不需要修改 Cargo.toml 来包含 std
  • 需要使用 use 将 std 中的特定条目引入当前作用域

使用嵌套路径清理大量的 use 语句

路径相同的部分::{路径差异的部分}

1
2
use std::{cmp::Ordering,io};
fn main()

如果两个 use 路径之一是另一个的子路径

使用 self

1
2
3
//use std::io;
//use std::io::Write;
use std::io::{self,Write}

通配符*

使用*可以把路径中所有的公共条目都引入到作用域

谨慎使用

应用场景:

  • prelude
  • 测试,将所有被测试代码引入到 tests 模块

将模块拆分为不同的文件

模块定义时,如果模块名后面时”;”,而不是代码块

  • Rust 会从模块同名的文件中加载内容
  • 模块树不会发生变化

示例:

文件名: src/lib.rs

1
2
3
4
5
6
7
8
9
mod front_of_house;

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}

示例 :声明 frontof_house 模块,其内容将位于 _src/front_of_house.rs

src/front_of_house.rs 会获取 front_of_house 模块的定义内容,如示例所示。

文件名: src/front_of_house.rs

1
2
3
pub mod hosting {
pub fn add_to_waitlist() {}
}

示例:

定义模块 front_of_house

方式一:直接在文件中写模块内容

文件:src/front_of_house.rs

1
2
3
pub mod hosting {
pub fn add_to_waitlist() {}
}
  • 这里 front_of_house 模块直接定义了子模块 hosting
  • 好处:简单,模块小的时候可以这样写。

方式二:把子模块拆到单独文件

  1. front_of_house.rs 中只声明子模块:
1
pub mod hosting;
  1. 创建目录和文件结构:
1
2
3
4
5
src/
├── lib.rs
├── front_of_house.rs ← front_of_house 模块的主体
└── front_of_house/ ← front_of_house 的子模块目录
└── hosting.rs ← 子模块 hosting 的实现
  1. hosting.rs 中写实际内容:
1
pub fn add_to_waitlist() {}
  • 这时 hosting 的内容完全放到 hosting.rs
  • 好处:模块大时,拆开文件更清晰、可维护。

传统风格

Rust Edition 2018 前,使用下面这种风格

1
2
3
4
src/
└── front_of_house/
├── mod.rs // front_of_house 模块本体
└── hosting.rs // front_of_house 的子模块

常见集合

Vector

Vec 叫做 vector

创建 vector

Vec::new 函数

let v:Vec=Vec::new();

使用初始值创建 Vec,使用vec!

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let arr: [u8; 3] = [1, 2, 3];

let v = Vec::from(arr);
is_vec(v);

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

// vec!(..) 和 vec![..] 是同样的宏,宏可以使用 []、()、{}三种形式,因此...
let v = vec!(1, 2, 3);
is_vec(v);

// ...在下面的代码中, v 是 Vec<[u8; 3]> , 而不是 Vec<u8>
let v1 = vec!(arr);

添加元素

1
2
let mut v:Vec<i32>=Vec::new();
v.push(1);

删除 Vector

类似于任何其他的 struct,vector 在其离开作用域时会被释放

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

// 处理变量 v
} // <- 这里 v 离开作用域并被丢弃

读取 Vector 中的值

  • 索引方式
  • get 方法
1
2
3
4
5
6
7
8
9
let v = vec![1, 2, 3, 4, 5];

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

match v.get(2) {
Some(third) => println!("The third element is {}", third),
None => println!("There is no third element."),
}
  • 使用索引方法访问超出数组元素的值时,程序会 panic
  • 而使用 get 方法访问时程序会返回一个 None
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let mut v = Vec::from([1, 2, 3]);
for i in 0..5 {
println!("{:?}", v.get(i))
}

for i in 0..5 {
if let Some(x) = v.get(i) {
v[i] = x + 1
} else {
v.push(i + 2)
}
}

assert_eq!(format!("{:?}",v), format!("{:?}", vec![2, 3, 4, 5, 6]));

println!("Success!")
}

引用借用规则

当我们获取了 vector 的第一个元素的不可变引用并尝试在 vector 末尾增加一个元素的时候,如果尝试在函数的后面引用这个元素是行不通的

1
2
3
4
5
6
7
let mut v = vec![1, 2, 3, 4, 5];

let first = &v[0];//不可变的借用

v.push(6);//可变的借用

println!("The first element is: {}", first);//不可变的借用

为什么第一个元素的引用会关心 vector 结尾的变化?不能这么做的原因是由于 vector 的工作方式:在 vector 的结尾增加新元素时,在没有足够空间将所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。这时,第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况

遍历 Vector

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

我们也可以遍历可变 vector 的每一个元素的可变引用以便能改变他们

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

为了修改可变引用所指向的值,在使用 += 运算符之前必须使用解引用运算符(*)获取 i 中的值。

扩展 Vector

Vec 可以使用 extend 方法进行扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() {
let mut v1 = Vec::from([1, 2, 4]);
v1.pop();
v1.push(3);
// v1 is [1,2,3]

let mut v2 = Vec::new();
v2.extend([1, 2, 3]);
// v2 is [1,2,3]

assert_eq!(format!("{:?}",v1), format!("{:?}",v2));

println!("Success!")
}

使用 enum 来使 Vec 存储多种数据类型

定义一个枚举,以便能在 vector 中存放不同类型的数据

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),
];

使用特征对象来使 Vec 存储多种数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
trait IpAddr {
fn display(&self);
}

struct V4(String);

impl IpAddr for V4 {
fn display(&self) {
println!("ipv4: {:?}",self.0)
}
}

struct V6(String);

impl IpAddr for V6 {
fn display(&self) {
println!("ipv6: {:?}",self.0)
}
}

fn main() {
// 填空
let v: Vec<Box<dyn IpAddr>> = vec![
Box::new(V4("127.0.0.1".to_string())),
Box::new(V6("::1".to_string())),
];

for ip in v {
ip.display();
}
}

将 X 类型转换(From/Into 特征)成 Vec

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn main() {
// array -> Vec
let arr = [1, 2, 3];
let v1 = Vec::from(arr);
let v2: Vec<i32> = arr.into();

assert_eq!(v1, v2);


// String -> Vec
let s = "hello".to_string();
let v1: Vec<u8> = s.into();

let s = "hello".to_string();
let v2 = s.into_bytes();
assert_eq!(v1, v2);

let s = "hello";
let v3 = Vec::from(s);
assert_eq!(v2, v3);

println!("Success!")
}

切片

与 String 的切片类似, Vec 也可以使用切片。如果说 Vec 是可变的,那它的切片就是不可变或者说只读的,我们可以通过 & 来获取切片。

在 Rust 中,将切片作为参数进行传递是更常见的使用方式,例如当一个函数只需要可读性时,那传递 Vec 或 String 的切片 &[T] / &str 会更加适合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
let mut v = vec![1, 2, 3];

let slice1 = &v[..];
// 越界访问将导致 panic.
// 修改时必须使用 `v.len`
let slice2 = &v[0..v.len()];

assert_eq!(slice1, slice2);

// 切片是只读的
// 注意:切片和 `&Vec` 是不同的类型,后者仅仅是 `Vec` 的引用,并可以通过解引用直接获取 `Vec`
let vec_ref: &mut Vec<i32> = &mut v;
(*vec_ref).push(4);
let slice3 = &mut v[0..];
// slice3.push(4);

assert_eq!(slice3, &[1, 2, 3, 4]);

println!("Success!")
}

容量

容量 capacity 是已经分配好的内存空间,用于存储未来添加到 Vec 中的元素。而长度 len 则是当前 Vec 中已经存储的元素数量。如果要添加新元素时,长度将要超过已有的容量,那容量会自动进行增长:Rust 会重新分配一块更大的内存空间,然后将之前的 Vec 拷贝过去,因此,这里就会发生新的内存分配

若这段代码会频繁发生,那频繁的内存分配会大幅影响我们系统的性能,最好的办法就是提前分配好足够的容量,尽量减少内存分配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
fn main() {
let mut vec = Vec::with_capacity(10);

assert_eq!(vec.len(), 0);
assert_eq!(vec.capacity(), 10);

// 由于提前设置了足够的容量,这里的循环不会造成任何内存分配...
for i in 0..10 {
vec.push(i);
}
assert_eq!(vec.len(), 10);
assert_eq!(vec.capacity(), 10);

// ...但是下面的代码会造成新的内存分配
vec.push(11);
assert_eq!(vec.len(), 11);
assert!(vec.capacity() >= 11);


// 填写一个合适的值,在 `for` 循环运行的过程中,不会造成任何内存分配
let mut vec = Vec::with_capacity(100);
for i in 0..100 {
vec.push(i);
}

assert_eq!(vec.len(), 100);
assert_eq!(vec.capacity(), 100);

println!("Success!")
}

只要为 Vec 实现了 From 特征,那么 T 就可以被转换成 Vec。

String

  • 字符串是 Byte 的集合
  • UFT-8 编码
  • 一些方法能将 byte 解析为文本

字符串是什么?

Rust 的核心语言层面,只有一个字符串类型:字符串切片 str(或者&str)

  • 字符串切片:对存储在其它地方,UTF-8 编码的字符串引用
  • 字符串的字面值:存储在二进制文件中,也是字符串切片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let s = String::from("hello, 世界");
let slice1 = &s[0..1];
//提示: `h` 在 UTF-8 编码中只占用 1 个字节
assert_eq!(slice1, "h");

let slice2 = &s[7..10];// 提示: `中` 在 UTF-8 编码中占用 3 个字节
assert_eq!(slice2, "世");

// 迭代 s 中的所有字符
for (i, c) in s.chars().enumerate() {
if i == 7 {
assert_eq!(c, '世')
}
}

println!("Success!")
}

事实上 String 是一个智能指针,它作为一个结构体存储在栈上,然后指向存储在堆上的字符串底层数据。

存储在栈上的智能指针结构体由三部分组成:一个指针只指向堆上的字节数组,已使用的长度以及已分配的容量 capacity (已使用的长度小于等于已分配的容量,当容量不够时,会重新分配内存空间)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::mem;

fn main() {
let story = String::from("Rust By Practice");

// 阻止 String 的数据被自动 drop
let mut story = mem::ManuallyDrop::new(story);

let ptr = story.as_mut_ptr();
let len = story.len();
let capacity = story.capacity();

assert_eq!(16, len);

// 我们可以基于 ptr 指针、长度和容量来重新构建 String.
// 这种操作必须标记为 unsafe,因为我们需要自己来确保这里的操作是安全的
let s = unsafe { String::from_raw_parts(ptr, len, capacity) };

assert_eq!(*story, s);

println!("Success!")
}

其它字符串类型

String 类型

来自标准库,也是 UTF-8 编码

String vs Str:拥有或借用的变体

其他字符串类型

OsString / OsStr

1
2
3
4
5
use std::ffi::OsString;
fn main() {
let mut os_string = OsString::from("hello");
os_string.push(" world"); // 可变
}

主要用于处理文件路径、命令行参数、环境变量等系统字符串。

CString / CStr

1
2
3
use std::ffi::CString;

let c_string = CString::new("hello").expect("NUL found!");

来自 std::ffi,用于 与 C 语言接口交互

特点

  • NUL 结尾 (\0) 的字符串。
  • CString 拥有数据,保证没有内部 NUL。
  • CStr 是不可变借用,类似于 &str

CString 没有提供类似 push_str 或索引访问的方法修改内容,一旦创建,就不能修改内部字节

String 与&str 的转换

使用 to_string()

1
2
3
4
5
6
7
8
fn main() {
let s = "hello, world".to_string();
greetings(s)
}

fn greetings(s: String) {
println!("{}",s)
}

使用 String::from()

1
2
3
4
5
6
7
8
fn main() {
let s = String::from("hello, world");
greetings(s)
}

fn greetings(s: String) {
println!("{}",s)
}

字符串转义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
// 你可以使用转义的方式来输出想要的字符,这里我们使用十六进制的值,例如 \x73 会被转义成小写字母 's'
// 填空以输出 "I'm writing Rust"
let byte_escape = "I'm writing Ru\x73__!";
println!("What are you doing\x3F (\\x3F means ?) {}", byte_escape);

// 也可以使用 Unicode 形式的转义字符
let unicode_codepoint = "\u{211D}";
let character_name = "\"DOUBLE-STRUCK CAPITAL R\"";

println!("Unicode character {} (U+211D) is called {}",
unicode_codepoint, character_name );

// 还能使用 \ 来连接多行字符串
let long_string = "String literals
can span multiple lines.
The linebreak and indentation here \
can be escaped too!";
println!("{}", long_string);
}

有时候需要转义的字符很多,我们会希望使用更方便的方式来书写字符串: raw string.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
fn main() {
let raw_str = r"Escapes don't work here: \x3F \u{211D}";
println!("{}", raw_str);

// 如果字符串包含双引号,可以在开头和结尾加 #
let quotes = r#"And then I said: "There is no escape!""#;
println!("{}", quotes);

// 如果还是有歧义,可以继续增加,没有限制
let longer_delimiter = r###"A string with "# in it. And even "##!"###;
println!("{}", longer_delimiter);
}
fn main() {
let raw_str = "Escapes don't work here: \x3F \u{211D}";
assert_eq!(raw_str, "Escapes don't work here: ? ℝ");

// If you need quotes in a raw string, add a pair of #s
let quotes = r#"And then I said: "There is no escape!""#;
println!("{}", quotes);

// If you need "# in your string, just use more #s in the delimiter.
// You can use up to 65535 #s.
let delimiter = r###"A string with "# in it. And even "##!"###;
println!("{}", delimiter);

// Fill the blank
let long_delimiter = r###"Hello, "##""###;
assert_eq!(long_delimiter, "Hello, \"##\"")
}

这里 r#”标记一个原始字符串的开始,”#标记一个字符串的结束,如果还是有歧义可以继续加#

创建一个新的 String

String::new()

let mut s = String::new();

使用 to_string()

1
2
3
4
5
6
let data = "initial contents";

let s = data.to_string();

// 该方法也可直接用于字符串字面值:
let s = "initial contents".to_string();

也可以使用 string::from()

let s = String::from(“initial contents”);

更新 String

push_str()

1
2
let mut s = String::from("foo");
s.push_str("bar");

push_str()方法不会获得参数的所有权

1
2
3
4
5
6
7
8
9
let mut s1 = String::from("foo");
let s2 = "bar";
s1.push_str(s2);
println!("s2 is {}", s2);

let mut s1 = String::from("foo");
let s2 = "bar";
s1.push_str(&s2);
println!("s2 is {}", s2);

push()

push 方法被定义为获取一个单独的字符作为参数,并附加到 String 中

1
2
let mut s = String::from("lo");
s.push('l');

+连接字符串

只能将 String &str 类型进行拼接,并且 String 的所有权在此过程中会被 move

1
2
3
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // 注意 s1 被移动了,不能继续使用

+ 运算符使用了 add 函数,这个函数签名看起来像这样:

1
fn add(self, s: &str) -> String {

这并不是标准库中实际的签名;

但是&s2 的类型是 &String 而不是 &str。那么为什么还能编译呢

之所以能够在 add 调用中使用 &s2 是因为 &String 可以被 强转coerced)成 &str。当 add 函数被调用时,Rust 使用了一个被称为 Deref 强制转换deref coercion)的技术,你可以将其理解为它把 &s2 变成了 &s2[..]。

format!

1
2
3
4
5
6
7
8
9
10
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = s1 + "-" + &s2 + "-" + &s3;
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = format!("{}-{}-{}", s1, s2, s3);

宏 format! 生成的代码使用引用所以不会获取任何参数的所有权

s1, s2, s3 都是借用。

Rust 会 把内容复制到新字符串(堆上分配):

  • 原来的字符串不受影响。
  • 这里的复制是必需的,因为生成了新的独立字符串。

按索引的形式进行访问String

Rust 的字符串不支持索引语法访问

内部表现

String 是一个 Vec 的封装。

let hello = String::from(“Hola”);

在这里,len 的值是 4 ,这意味着储存字符串 “Hola” 的 Vec 的长度是四个字节:这里每一个字母的 UTF-8 编码都占用一个字节。

(注意这个字符串中的首字母是西里尔字母的 Ze 而不是阿拉伯数字 3 。)

1
let hello = String::from("Здравствуйте");

当问及这个字符是多长的时候有人可能会说是 12。然而,Rust 的回答是 24。这是使用 UTF-8 编码 “Здравствуйте” 所需要的字节数,这是因为每个 Unicode 标量值需要两个字节存储。

因此一个字符串字节值的索引并不总是对应一个有效的 Unicode 标量值

字节字符串

字节字符串或者说字节数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
use std::str;

fn main() {
// 注意,这并不是 `&str` 类型了!
let bytestring: &[u8; 21] = b"this is a byte string";


// 字节数组没有实现 `Display` 特征,因此只能使用 `Debug` 的方式去打印
println!("A byte string: {:?}", bytestring);

// 字节数组也可以使用转义
let escaped = b"\x52\x75\x73\x74 as bytes";
// ...但是不支持 unicode 转义
// let escaped = b"\u{211D} is not allowed";
println!("Some escaped bytes: {:?}", escaped);


// raw string
let raw_bytestring = br"\u{211D} is not escaped here";
println!("{:?}", raw_bytestring);

// 将字节数组转成 `str` 类型可能会失败
if let Ok(my_str) = str::from_utf8(raw_bytestring) {
println!("And the same as text: '{}'", my_str);
}

let _quotes = br#"You can also use "fancier" formatting, \
like with normal raw strings"#;

// 字节数组可以不是 UTF-8 格式
let shift_jis = b"\x82\xe6\x82\xa8\x82\xb1\x82\xbb"; // "ようこそ" in SHIFT-JIS

// 但是它们未必能转换成 `str` 类型
match str::from_utf8(shift_jis) {
Ok(my_str) => println!("Conversion successful: '{}'", my_str),
Err(e) => println!("Conversion failed: {:?}", e),
};
}

字节,标量值,字型簇

Rust 有三种看待字符串的方式:

  • 字节 Byte
1
2
3
4
5
6
fn main(){
let w = "नमस्ते";
for b in w.bytes(){
println!("{}",b);
}
}

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
224
164
168
224
164
174
224
164
184
224
165
141
224
164
164
224
165
135
  • 标量值 Scalar Values
1
2
3
4
5
6
fn main(){
let w = "नमस्ते";
for b in w.chars(){
println!("{}",b);
}
}

输出

1
2
3
4
5
6






  • 字形簇 Grapheme Clusters(最接近所谓的字母)

获取比较复杂,标准库中已经不提供了。 需要借助第三方库。

Rust 不允许对 String 进行索引的一个原因:

索引操作应该小号一个常量时间 O(1)

而 String 无法保证:需要遍历所有的内容,来确定有多少个合法的字符

切割 String

字符串索引应该返回的类型是不明确的:字节值、字符、字形簇或者字符串 slice。因此,如果你真的希望使用索引创建字符串 slice 时,Rust 会要求你更明确一些。为了更明确索引并表明你需要一个字符串 slice,相比使用 [] 和单个值的索引,可以使用 [] 和一个 range 来创建含特定字节的字符串 slice:

1
2
3
let hello = "Здравствуйте";

let s = &hello[0..4];

s 会是一个 &str,它包含字符串的头四个字节。早些时候,我们提到了这些字母都是两个字节长的,所以这意味着 s 将会是 “Зд”。

如果获取 &hello[0..1] 会发生什么呢?答案是:Rust 在运行时会 panic

因此切割时不能跨越字符串边界

遍历字符串

无法通过索引的方式去访问字符串中的某个字符,但是可以使用切片的方式 &s1[start..end] ,但是 start 和 end 必须准确落在字符的边界处.

  • 对于标量值: chars()方法
  • 对于字节: bytes()方法、
  • 对于字形簇:很复杂,标准库未提供,中英文都不需要关注字符簇,利用 chars 就可以遍历中英文
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let s1 = String::from("hi,中国");
let h = &s1[0..1];
assert_eq!(h, "h");

let h1 = &s1[3..6];
assert_eq!(h1, "中");
}
fn main() {
for c in "你好,世界".chars() {
println!("{}", c)
}
}
//output:
//你
//好
//,
//世
//界

HashMap

HashMap 默认使用 SipHash 1-3 哈希算法,该算法对于抵抗 HashDos 攻击非常有效。在性能方面,如果你的 key 是中型大小的,那该算法非常不错,但是如果是小型的 key( 例如整数 )亦或是大型的 key ( 例如字符串 ),那你需要采用社区提供的其它算法来提高性能。

哈希表的算法是基于 Google 的 SwissTable,你可以在这里找到 C++ 的实现。

创建 HashMap

创建空 HashMap:new()函数

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

let mut scores = HashMap::new();
//let mut scores:HashMap<String,i32> = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
  • HashMap 用的较少,不在 Prelude 中
  • 标准库对其支持较少,没有内置的宏来创建 HashMap
  • 数据存在 heap 中
  • 同构的,即 K 必须为一种类型,V 为另一种类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::collections::HashMap;
fn main() {
let teams = [
("Chinese Team", 100),
("American Team", 10),
("France Team", 50),
];

let mut teams_map1 = HashMap::new();
for team in &teams {
teams_map1.insert(team.0, team.1);
}

let teams_map2: HashMap<_,_> = teams.into_iter().collect();
// let teams_map2 = HashMap::from(teams);
assert_eq!(teams_map1, teams_map2);

println!("Success!")
}

collect 方法创建 HashMap

collect 方法可以将数据收集进一系列的集合类型

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

let teams = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];

let mut scores: HashMap<_, _> =
teams.into_iter().zip(initial_scores.into_iter()).collect();

如果队伍的名字和初始分数分别在两个 vector 中,可以使用 zip 方法来创建一个元组的迭代器,其中 “Blue” 与 10 是一对,依此类推。接着就可以使用 collect 方法将这个元组的迭代器转换成一个 HashMap

HashMap 和所有权

  • 对于实现了 Copy trait 的类型(如 i32),值会被复制到 HashMap 中
  • 对于拥有所有权的值(例如 String),值会被移动,所有权会转移给 HashMap
  • 如果把引用插入到 HashMap,值本身不会移动但是在 HashMap 有效的期间,被引用的值必须保持有效

访问 HashMap 中的值

get 方法

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);

for 循环遍历 HashMap

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);
}

这会以任意顺序打印出每一个键值对:

1
2
Yellow: 50
Blue: 10

索引与 get 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert("Sunface", 98);
scores.insert("Daniel", 95);
scores.insert("Ashley", 69);
scores.insert("Katie", 58);

// get 返回一个 Option<&V> 枚举值
let score = scores.get("Sunface");
assert_eq!(score, Some(&98));

if scores.contains_key("Daniel") {
// 索引返回一个值 V
let score = scores["Daniel"];
assert_eq!(score, 95);
scores.remove("Daniel");
}

assert_eq!(scores.len(), 3);

for (name, score) in scores {
println!("The score of {} is {}", name, score)
}
}

更新 HashMap

覆盖一个值

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

let mut scores = HashMap::new();

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

println!("{:?}", scores);

这会打印出 {“Blue”: 25}。原始的值 10 则被覆盖了

只在键没有对应值时插入

使用 entry 方法只在键没有对应一个值时插入

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

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);

let e=scores.entry(String::from("Yellow"));
println!("{:?}",e);
e.or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);

println!("{:?}", scores);

输出:

1
2
Entry(VacantEntry("Yellow"))
{"Blue": 10, "Yellow": 50}

entry 方法

检查指定的 K 是否对应一个 V

参数为 K,返回 enum Entry:代表值是否存在

Entry 的or_insert()方法:

返回:

  • 如果 K 存在,返回到对应的 V 的一个可变引用
  • 如果 K 不存在,将方法参数作为 K 的新值插进去,返回到这个值的可变引用

根据旧值更新一个值

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

let text = "hello world wonderful world";

let mut map = HashMap::new();

for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}

println!("{:?}", map);

这里 or_insert 返回的是一个可变引用,指向刚 entry 方法插入的键对应的值

哈希函数

HashMap 默认使用一种叫做 SipHash 的哈希函数,它可以抵御涉及哈希表(hash table)1 的拒绝服务(Denial of Service, DoS)攻击。然而这并不是可用的最快的算法,不过为了更高的安全性值得付出一些性能的代价。如果性能监测显示此哈希函数非常慢,以致于你无法接受,你可以指定一个不同的 hasher 来切换为其它函数。hasher 是一个实现了 BuildHasher trait 的类型。我们并不需要从头开始实现你自己的 hasher;crates.io 上有许多常用哈希算法的 hasher 的库。

HashMap key 的限制

任何实现了 Eq 和 Hash 特征的类型都可以用于 HashMap 的 key,包括:

  • bool (虽然很少用到,因为它只能表达两种 key)
  • int, uint 以及它们的变体,例如 u8、i32 等
  • String 和 &str (提示: HashMap 的 key 是 String 类型时,你其实可以使用 &str 配合 get 方法进行查询

需要注意的是,f32 和 f64 并没有实现 Hash,原因是 浮点数精度 的问题会导致它们无法进行相等比较。

如果一个集合类型的所有字段都实现了 Eq 和 Hash,那该集合类型会自动实现 Eq 和 Hash。例如 Vec 要实现 Hash,那么首先需要 T 实现 Hash。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 提示: `derive` 是实现一些常用特征的好办法
use std::collections::HashMap;

#[derive(Hash, Eq, PartialEq, Debug)]
struct Viking {
name: String,
country: String,
}

impl Viking {
fn new(name: &str, country: &str) -> Viking {
Viking {
name: name.to_string(),
country: country.to_string(),
}
}
}

fn main() {
// 使用 HashMap 来存储 viking 的生命值
let vikings = HashMap::from([
(Viking::new("Einar", "Norway"), 25),
(Viking::new("Olaf", "Denmark"), 24),
(Viking::new("Harald", "Iceland"), 12),
]);

// 使用 derive 的方式来打印 viking 的当前状态
for (viking, health) in &vikings {
println!("{:?} has {} hp", viking, health);
}
}

容量

关于容量,我们在之前的 Vector 中有详细的介绍,而 HashMap 也可以调整容量: 你可以通过 HashMap::with_capacity(uint) 使用指定的容量来初始化,或者使用 HashMap::new() ,后者会提供一个默认的初始化容量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use std::collections::HashMap;
fn main() {
let mut map: HashMap<i32, i32> = HashMap::with_capacity(100);
map.insert(1, 2);
map.insert(3, 4);
// 事实上,虽然我们使用了 100 容量来初始化,但是 map 的容量很可能会比 100 更多
assert!(map.capacity() >= 100);

// 对容量进行收缩,你提供的值仅仅是一个允许的最小值,实际上,Rust 会根据当前存储的数据量进行自动设置,当然,这个值会尽量靠近你提供的值,同时还可能会预留一些调整空间

map.shrink_to(50);
assert!(map.capacity() >= 50);

// 让 Rust 自行调整到一个合适的值,剩余策略同上
map.shrink_to_fit();
assert!(map.capacity() >= 2);
println!("Success!")
}

所有权

对于实现了 Copy 特征的类型,例如 i32,那类型的值会被拷贝到 HashMap 中。而对于有所有权的类型,例如 String,它们的值的所有权将被转移到 HashMap 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use std::collections::HashMap;
fn main() {
let v1 = 10;
let mut m1 = HashMap::new();
m1.insert(v1, v1);
println!("v1 is still usable after inserting to hashmap : {}", v1);

let v2 = "hello".to_string();
let mut m2 = HashMap::new();
// 所有权在这里发生了转移
m2.insert(v2, v1);

assert_eq!(v2, "hello");

println!("Success!")
}

第三方 Hash 库

在开头,我们提到过如果现有的 SipHash 1-3 的性能无法满足需求,那么可以使用社区提供的替代算法。

例如其中一个社区库的使用方式如下:

1
2
3
4
5
6
7
8
9
use std::hash::BuildHasherDefault;
use std::collections::HashMap;
// 引入第三方的哈希函数
use twox_hash::XxHash64;


let mut hash: HashMap<_, _, BuildHasherDefault<XxHash64>> = Default::default();
hash.insert(42, "the answer");
assert_eq!(hash.get(&42), Some(&"the answer"));

错误处理

大部分情况下,在编译时提示错误,并处理

错误的分类

  • 可恢复:

例如文件未找到,可再次尝试

  • 不可恢复

bug,例如访问索引超出范围

Rust 没有类似 C++/Java 的异常机制

  • 可恢复的错误:Result
  • 不可恢复:panic!宏

不可恢复的错误与 panic!

当 panic!宏执行

  • 程序打印一个错误信息
  • 展开(unwind),清理调用栈(Stack)
  • 退出程序

panic = ‘abort’

默认情况下,当 panic 发生

  • 程序展开调用栈(工作量大)

Rust 沿着调用栈往回走,清理每个遇到的函数中的数据

  • 或立即中止调用栈 abort

不进行清理,直接停止程序,内存需要由 OS 进行清理

想让二进制文件更小,把设置从“展开”改为“中止”

1
2
[profile.release]
panic = 'abort'

自己写的代码中内 panic

1
2
3
fn main(){
panic!("crash and burn");
}

所以依赖的代码中:外部 panic

1
2
3
4
fn main(){
let vector=vec![1,2,3];
vector[100];
}

输出

1
2
3
4
5
6
Compiling demo v0.1.0 (C:\Users\cauchy\Desktop\rust\demo)
Finished dev [unoptimized + debuginfo] target(s) in 0.18s
Running `target\debug\demo.exe`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 100', src\main.rs:3:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\demo.exe` (exit code: 101)

通过调用 panic!的函数的回溯信息来定位引起问题的代码

使用以下命令运行代码(Windows cmd)

1
set RUST_BACKTRACE=1 && cargo run

Windows Poweshell

1
$Env:RUST_BACKTRACE=1 -and (cargo run)

linux 下

1
export RUST_BACKTRACE=1 && cargo run

更详细的信息

RUST_BACKTRACE=full

为了获取带有调试信息的回溯,必须启用调试符号(不带—release)

Result 枚举与可恢复的错误

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

T 代表成功时返回的 Ok 成员中的数据的类型,

而 E 代表失败时返回的 Err 成员中的错误的类型

Result 及其变体也是由 prelude 带入作用域的

打开文件

1
2
3
4
5
6
7
8
9
10
use std::fs::File;

fn main() {
let f = File::open("hello.txt");

let f = match f {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {:?}", error),
};
}

匹配不同的错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::fs::File;
use std::io::ErrorKind;

fn main() {
let f = File::open("hello.txt");

let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => {
panic!("Problem opening the file: {:?}", other_error)
}
},
};
}

使用 unrap_or_else()闭包(closure)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std::fs::File;
use std::io::ErrorKind;

fn main() {
let f = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {:?}", error);
})
} else {
panic!("Problem opening the file: {:?}", error);
}
});
}

unwrap

match 表达式的一个快捷方法

1
2
3
4
5
use std::fs::File;

fn main() {
let f = File::open("hello.txt").unwrap();
}

相当于

1
2
3
4
5
6
7
8
9
10
use std::fs::File;

fn main() {
let f = File::open("hello.txt");

let f = match f {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {:?}", error),
};
}
  • 如果 Result 结果是 Ok,返回 Ok 里面的值
  • 如果 Result 结果是 Err,调用 panic!宏

expect

可自定义错误信息的 unwrap

1
2
3
4
5
use std::fs::File;

fn main() {
let f = File::open("hello.txt").expect("无法打开文件");
}

传播错误

将错误传播给调用者

自定义实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/main.rs

use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
let f = File::open("hello.txt");

let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};

let mut s = String::new();

match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}

?运算符

1
2
3
4
5
6
7
8
9
10
use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}

? 被定义为与自定义传播错误的示例中定义的处理 Result 值的 match 表达式有着完全相同的工作方式。

如果 Result 的值是 Ok,这个表达式将会返回 Ok 中的值而程序将继续执行。

如果值是 Err,Err 中的值将作为整个函数的返回值,就好像使用了 return 关键字一样,这样错误值就被传播给了调用者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
use std::fs::File;
use std::io::{self, Read};

fn read_file1() -> Result<String, io::Error> {
let f = File::open("hello.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};

let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}

fn read_file2() -> Result<String, io::Error> {
let mut s = String::new();

File::open("hello.txt")?.read_to_string(&mut s)?;

Ok(s)
}

fn main() {
assert_eq!(read_file1().unwrap_err().to_string(), read_file2().unwrap_err().to_string());
println!("Success!")
}

?与 from 函数

Trait std::convert::From 上的 from 函数

  • 用于错误之间的转换
  • 被?所应用的错误,会隐式地被 from 函数处理
  • 当? 调用 from 函数时:

它所接收地错误类型会被转化为当前函数返回类型所定义的错误类型

用于: 针对不同的错误原因,返回同一种错误类型

只要每个错误类型实现了转换为所返回的错误类型的 from 函数

可以在 ? 之后直接使用链式方法调用来进一步缩短代码

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();

File::open("hello.txt")?.read_to_string(&mut s)?;

Ok(s)
}

自定义实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
use std::fmt;
use std::fs::File;
use std::io::{self, Read};


#[derive(Debug)]
enum MyError {
Io(io::Error),
Parse,
}

// 为 MyError 实现 Display(可选但常用)
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MyError::Io(e) => write!(f, "IO error: {}", e),
MyError::Parse => write!(f, "Parse error"),
}
}
}

impl From<io::Error> for MyError {
fn from(error: io::Error) -> MyError {
MyError::Io(error)
}
}


fn read_username_from_file() -> Result<String, MyError> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}

?与 main 函数

main 函数的返回类型为()类型

1
2
3
4
5
6
7
use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
let f = File::open("hello.txt")?;
Ok(())
}

map,and_then

map 示例 —— 修改 Ok 里的值

只处理 Ok 的值并返回新的 Result,不会改变错误类型

1
2
3
let x: Result<i32, &str> = Ok(2);
let y = x.map(|n| n * 3);
assert_eq!(y, Ok(6));

如果是错误呢?

1
2
3
let x: Result<i32, &str> = Err("error");
let y = x.map(|n| n * 3);
assert_eq!(y, Err("error")); // 错误会原样返回

and_then 示例 —— 链式 Result 操作

and_then处理 Ok,并继续返回一个 Result(链式逻辑), 适合把多个可能出错的步骤串起来:

1
2
3
4
5
6
7
8
9
10
fn sq_then_to_string(x: i32) -> Result<String, &'static str> {
if x < 0 {
Err("negative")
} else {
Ok((x * x).to_string())
}
}

let result = Ok(3).and_then(sq_then_to_string);
assert_eq!(result, Ok("9".to_string()));

如果出错,自动停止链式计算:

1
2
let result = Err("bad").and_then(sq_then_to_string);
assert_eq!(result, Err("bad"));

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
use std::num::ParseIntError;

// With the return type rewritten, we use pattern matching without `unwrap()`.
// But it's so Verbose..
fn multiply(n1_str: &str, n2_str: &str) -> Result<i32, ParseIntError> {
match n1_str.parse::<i32>() {
Ok(n1) => {
match n2_str.parse::<i32>() {
Ok(n2) => {
Ok(n1 * n2)
},
Err(e) => Err(e),
}
},
Err(e) => Err(e),
}
}

// Rewriting `multiply` to make it succinct
// You MUST USING `and_then` and `map` here
fn multiply1(n1_str: &str, n2_str: &str) -> Result<i32, ParseIntError> {
// IMPLEMENT...
n1_str.parse::<i32>().and_then(|n1| {
n2_str.parse::<i32>().map(|n2| n1 * n2)
})
}

fn print(result: Result<i32, ParseIntError>) {
match result {
Ok(n) => println!("n is {}", n),
Err(e) => println!("Error: {}", e),
}
}

fn main() {
// This still presents a reasonable answer.
let twenty = multiply1("10", "2");
print(twenty);

// The following now provides a much more helpful error message.
let tt = multiply("t", "2");
print(tt);

println!("Success!")
}

何时 panic!

总体原则

  • 在定义一个可能失败的函数时,优先考虑返回 Result
  • 否则就 panic!

场景

  • 演示某些概念:unwrap
  • 原型代码:unwrap,expect
  • 测试:unwrap,expect

有时你比编译器掌握更多的信息

  • 你可以确定 Result 就是 Ok:unwrap
1
2
use std::net::IpAddr;
let home: IpAddr = "127.0.0.1".parse().unwrap();

调用你的代码,传入无意义的参数值:panic!

  • 调用外部不可控代码,返回非法状态,你无法修复:panic!

  • 如果失败是可预期的:Result

  • 当你的代码对值进行操作,首先应该验证这些值:panic! (assert!)

创建自定义类型进行有效性验证

一种实现方式是将猜测解析成 i32 而不仅仅是 u32,来默许输入负数,接着检查数字是否在范围内:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
loop {
// --snip--

let guess: i32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};

if guess < 1 || guess > 100 {
println!("The secret number will be between 1 and 100.");
continue;
}

match guess.cmp(&secret_number) {
// --snip--
}

if 表达式检查了值是否超出范围,告诉用户出了什么问题,并调用 continue 开始下一次循环,请求另一个猜测。if 表达式之后,就可以在知道 guess 在 1 到 100 之间的情况下与秘密数字作比较了。

相反我们可以创建一个新类型来将验证放入创建其实例的函数中,而不是到处重复这些检查。这样就可以安全的在函数签名中使用新类型并相信他们接收到的值。示例 中展示了一个定义 Guess 类型的方法,只有在 new 函数接收到 1 到 100 之间的值时才会创建 Guess 的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pub struct Guess {
value: i32,
}

impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {}.", value);
}

Guess { value }
}

pub fn value(&self) -> i32 {
self.value
}
}

我们实现了一个借用了 self 的方法 value,它没有任何其他参数并返回一个 i32。这类方法有时被称为 getter,因为它的目的就是返回对应字段的数据。这样的公有方法是必要的,因为 Guess 结构体的 value 字段是私有的。私有的字段 value 是很重要的,这样使用 Guess 结构体的代码将不允许直接设置 value 的值:调用者 必须 使用 Guess::new 方法来创建一个 Guess 的实例,这就确保了不会存在一个 value 没有通过 Guess::new 函数的条件检查的 Guess。

于是,一个接收(或返回) 1 到 100 之间数字的函数就可以声明为接收(或返回) Guess 的实例,而不是 i32,同时其函数体中也无需进行任何额外的检查。

在 fn main 中使用 Result

一个典型的 main 函数长这样:

1
2
3
fn main() {
println!("Hello World!");
}

事实上 main 函数还可以返回一个 Result 类型:如果 main 函数内部发生了错误,那该错误会被返回并且打印出一条错误的 debug 信息。

1
2
3
4
5
6
7
8
9
10
11
use std::num::ParseIntError;

fn main() -> Result<(), ParseIntError> {
let number_str = "10";
let number = match number_str.parse::<i32>() {
Ok(number) => number,
Err(e) => return Err(e),
};
println!("{}", number);
Ok(())
}

泛型

函数中定义泛型

寻找 vec 中的最大值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
fn largest_i32(list: &[i32]) -> i32 {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}

fn largest_char(list: &[char]) -> char {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}

fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest_i32(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest_char(&char_list);
println!("The largest char is {}", result);
}

使用泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}

fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result);
}

运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Compiling demo v0.1.0 (C:\Users\cauchy\Desktop\rust\demo)
error[E0369]: binary operation `>` cannot be applied to type `T`
--> src\main.rs:5:17
|
5 | if item > largest {
| ---- ^ ------- T
| |
| T
|
help: consider restricting type parameter `T`
|
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T {
| ++++++++++++++++++++++

For more information about this error, try `rustc --explain E0369`.
error: could not compile `demo` due to previous error

简单来说,这个错误表明 largest 的函数体不能适用于 T 的所有可能的类型

因为在函数体需要比较 T 类型的值,不过它只能用于我们知道如何排序的类型。为了开启比较功能,标准库中定义的 std::cmp::PartialOrd trait 可以实现类型的比较功能.

修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn largest<T>(list: &[T]) -> &T
where
T: PartialOrd,
{
let mut largest = &list[0];
for item in list {
if *item > *largest {// 这里不用加*也可以,因为rust为引用类型实现了PartialOrd,前提是被引用的类型 T: PartialOrd

largest = item;
}
}
largest
}

fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result);
}

结构体中定义泛型

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
struct Point<T> {
x: T,
y: T,
}

fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}

枚举中定义泛型

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

枚举也可以拥有多个泛型类型。

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

方法定义中的泛型

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Point<T> {
x: T,
y: T,
}

impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
impl Point<i32> {
fn x(&self) -> &i32 {
&self.x
}
}

fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
}
  • 把 T 放在 impl 关键字后,表示在类型 T 上实现方法:impl Point
  • 只针对具体类型实现方法:impl Point
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Point<T> {
x: T,
y: T,
}

impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}

fn main() {
let p = Point{x: 5.0_f32, y: 10.0_f32};
println!("{}",p.distance_from_origin())
}
  • struct 里的泛型类型参数可以和方法的泛型类型参数不同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Point<X1, Y1> {
x: X1,
y: Y1,
}

impl<X1, Y1> Point<X1, Y1> {
fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
Point {
x: self.x,
y: other.y,
}
}
}

fn main() {
let p1 = Point { x: 5, y: 10.4 };
let p2 = Point { x: "Hello", y: 'c' };

let p3 = p1.mixup(p2);

println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}

const 泛型

针对类型实现的泛型,所有的泛型都是为了抽象不同的类型,那有没有针对值的泛型?答案就是 Const 泛型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct ArrayPair<T, const N: usize> {
left: [T; N],
right: [T; N],
}

impl<T: Debug, const N: usize> Debug for ArrayPair<T, N> {
// ...
}
fn foo<const N: usize>() {}

fn bar<T, const M: usize>() {
foo::<M>(); // ok: 符合第一种
foo::<2021>(); // ok: 符合第二种
foo::<{20 * 100 + 20 * 10 + 1}>(); // ok: 符合第三种

foo::<{ M + 1 }>(); // error: 违背第三种,const 表达式中不能有泛型参数 M
foo::<{ std::mem::size_of::<T>() }>(); // error: 泛型表达式包含了泛型参数 T

let _: [u8; M]; // ok: 符合第一种
let _: [u8; std::mem::size_of::<T>()]; // error: 泛型表达式包含了泛型参数 T
}

fn main() {}
  • 目前,const 泛型参数只能使用以下形式的实参:

    • 一个单独的 const 泛型参数

    • 一个字面量 (i.e. 整数, 布尔值或字符).

    • 一个具体的 const 表达式( 表达式中不能包含任何 泛型参数)

  • const 泛型还能帮我们避免一些运行时检查,提升性能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pub struct MinSlice<T, const N: usize> {
pub head: [T; N],
pub tail: [T],
}

fn main() {
let slice: &[u8] = b"Hello, world";
let reference: Option<&u8> = slice.get(6);
// 我们知道 `.get` 返回的是 `Some(b' ')`
// 但编译器不知道
assert!(reference.is_some());

let slice: &[u8] = b"Hello, world";

// 当编译构建 MinSlice 时会进行长度检查,也就是在编译期我们就知道它的长度是 12
// 在运行期,一旦 `unwrap` 成功,在 `MinSlice` 的作用域内,就再无需任何检查
let minslice = MinSlice::<u8, 12>::from_slice(slice).unwrap();
let value: u8 = minslice.head[6];
assert_eq!(value, b' ')
}

`

是结构体类型的一部分,和数组类型一样,这意味着长度不同会导致类型不同: Array 和 Array 是不同的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#[allow(unused)]

struct Array<T, const N: usize> {
data : [T; N]
}

fn main() {
let arrays = [
Array{
data: [1, 2, 3],
},
Array {
data: [1, 2, 3],
},
Array {
data: [4,5,6]
}
];
}

填空

1
2
3
4
5
6
7
8
9
10
11
// 填空
fn print_array<__>(__) {
println!("{:?}", arr);
}
fn main() {
let arr = [1, 2, 3];
print_array(arr);

let arr = ["hello", "world"];
print_array(arr);
}

答案:

1
2
3
4
5
6
7
8
9
10
fn print_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) {
println!("{:?}", arr);
}
fn main() {
let arr = [1, 2, 3];
print_array(arr);

let arr = ["hello", "world"];
print_array(arr);
}

有时我们希望能限制一个变量占用内存的大小,例如在嵌入式环境中,此时 const 泛型参数的第三种形式 const 表达式 就非常适合:

下面的代码用到了 feature,需要 nightly 编译器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#![allow(incomplete_features)]
#![feature(generic_const_exprs)]

fn check_size<T>(val: T)
where
Assert<{ core::mem::size_of::<T>() < 768 }>: IsTrue,
{
//...
}

// fix the errors in main
fn main() {
check_size([0u8; 767]);
check_size([0i32; 191]);
check_size(["hello你好"; 47]); // &str is a string reference, containing a pointer and string length in it, so it takes two word long, in x86-64, 1 word = 8 bytes
check_size([(); 31].map(|_| "hello你好".to_string())); // String is a smart pointer struct, it has three fields: pointer, length and capacity, each takes 8 bytes
check_size(['中'; 191]); // A char takes 4 bytes in Rust
}

pub enum Assert<const CHECK: bool> {}

pub trait IsTrue {}

impl IsTrue for Assert<true> {}

泛型代码的性能

Rust 实现了泛型,使得使用泛型类型参数的代码相比使用具体类型并没有任何速度上的损失。

Rust 通过在编译时进行泛型代码的 单态化monomorphization)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//fn main(){
// let integer = Some(5);
// let float = Some(5.0);
//}
enum Option_i32 {
Some(i32),
None,
}

enum Option_f64 {
Some(f64),
None,
}

fn main() {
let integer = Option_i32::Some(5);
let float = Option_f64::Some(5.0);
}

在编译时,rust 会将 Option 泛型展开为 Option和 Option类型

Trait:定义共同行为

trait 告诉 Rust 编译器,某种类型具有哪些并且可以与其他类型共享的功能

定义一个 trait

把方法签名放在一起,来定义实现某种目的所必需的一组行为

  • 关键字:trait
  • 只有方法签名,没有具体实现

文件名: src/lib.rs

1
2
3
pub trait Summary {
fn summarize(&self) -> String;
}

在类型上实现 trait

与为类型实现方法类似

不同之处:impl Xxxx for Tweet{….}

文件: src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}

impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}

pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}

impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}

文件 src/main.rs

1
2
3
4
5
6
7
8
9
10
use demo::{Summary,Tweet};
fn main(){
let tweet = Tweet{
username: String::from("horse_ebook"),
content: String::from("of course,sa you probably..."),
reply:false,
retweet:false,
};
println!("1 new tweet:{}",tweet.summary());
}

demo 就是 Cargo.toml 文件中的[package]项的 name

实现 trait 的约束

可以在某个类型上实现某个 trait 的前提条件是:

  • 整个类型 这个 trait 是在本地 crate 里定义的

无法为外部类型实现外部 trait

这个限制是被称为 相干性coherence) 的程序属性的一部分,或者更具体的说是 孤儿规则orphan rule),其得名于不存在父类型。这条规则确保了其他人编写的代码不会破坏你代码,反之亦然。没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,而 Rust 将无从得知应该使用哪一个实现。

简而言之:类型和 trait 都不是你写的那你就没有资格给它们加 impl

trait 中的函数的默认实现

文件名: src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}

impl Summary for NewsArticle {
//fn summarize(&self) -> String {
// format!("{}, by {} ({})", self.headline, self.author, self.location)
//}
}

pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}

impl Summary for Tweet {
//默认实现的重写的实现
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}

默认实现允许调用相同 trait 中的其他方法哪怕这些方法没有默认实现

1
2
3
4
5
6
7
pub trait Summary {
fn summarize_author(&self) -> String;

fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}

注意:无法从方法的重写实现里面调用默认的实现

trait 作为参数

impl triat 语法:

适用于简单情况,是 trait bound 的语法糖

1
2
3
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}

trait bound 语法:

适用于复杂情况

1
2
3
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}

比较

1
pub fn notify(item1: &impl Summary, item2: &impl Summary) {}

这适用于 item1 和 item2 允许是不同类型的情况(只要它们都实现了 Summary)。不过如果你希望强制它们都是相同类型呢?这只有在使用 trait bound 时才有可能:

1
pub fn notify<T: Summary>(item1: &T, item2: &T) {}

使用+指定多个 traint bound

1
2
3
pub fn notify(item: &(impl Summary + Display)) {}

pub fn notify<T: Summary + Display>(item: &T) {}

使用 where 简化 trait bound

fn some_function(t: &T, u: &U) -> i32 {

使用 where 从句

1
2
3
4
fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug
{}

返回实现了 trait 的类型

也可以在返回值中使用 impl Trait 语法,来返回实现了某个 trait 的类型:

1
2
3
4
5
6
7
8
9
10
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
}
}

不过这只适用于返回单一类型的情况。例如,这段代码的返回值类型指定为返回 impl Summary,但是返回了 NewsArticle 或 Tweet 就行不通:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
NewsArticle {
headline: String::from(
"Penguins win the Stanley Cup Championship!",
),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
}
} else {
Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
}
}
}

这里尝试返回 NewsArticle 或 Tweet。这不能编译,因为 impl Trait 工作方式的限制。

可以使用 dyn trait 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
struct Sheep {}
struct Cow {}

trait Animal {
fn noise(&self) -> String;
}

impl Animal for Sheep {
fn noise(&self) -> String {
"baaaaah!".to_string()
}
}

impl Animal for Cow {
fn noise(&self) -> String {
"moooooo!".to_string()
}
}

// 返回一个类型,该类型实现了 Animal 特征,但是我们并不能在编译期获知具体返回了哪个类型
// 修复这里的错误,你可以使用虚假的随机,也可以使用特征对象
fn random_animal(random_number: f64) -> Box<dyn Animal> {
if random_number < 0.5 {
Box::new(Sheep {})
} else {
Box::new(Cow {})
}
}

fn main() {
let random_number = 0.234;
let animal = random_animal(random_number);
println!("You've randomly chosen an animal, and it says {}", animal.noise());
}

特征对象,在数组中使用特征对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
trait Bird {
fn quack(&self);
}

struct Duck;
impl Duck {
fn fly(&self) {
println!("Look, the duck is flying")
}
}
struct Swan;
impl Swan {
fn fly(&self) {
println!("Look, the duck.. oh sorry, the swan is flying")
}
}

impl Bird for Duck {
fn quack(&self) {
println!("{}", "duck duck");
}
}

impl Bird for Swan {
fn quack(&self) {
println!("{}", "swan swan");
}
}

fn main() {
// 填空
let birds :[Box<dyn Bird>;2]=[Box::new(Duck{}),Box::new(Swan{})];

for bird in birds {
bird.quack();
// 当 duck 和 swan 变成 bird 后,它们都忘了如何翱翔于天际,只记得该怎么叫唤了。。
// 因此,以下代码会报错
// bird.fly();
}
}

&dyn 和 Box

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
trait Draw {
fn draw(&self) -> String;
}

impl Draw for u8 {
fn draw(&self) -> String {
format!("u8: {}", *self)
}
}

impl Draw for f64 {
fn draw(&self) -> String {
format!("f64: {}", *self)
}
}

fn main() {
let x = 1.1f64;
let y = 8u8;

// draw x
draw_with_box(Box::new(x));

// draw y
draw_with_ref(&y);
}

fn draw_with_box(x: Box<dyn Draw>) {
x.draw();
}

fn draw_with_ref(x: &dyn Draw) {
x.draw();
}

静态分发和动态分发 Static and Dynamic dispatch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
trait Foo {
fn method(&self) -> String;
}

impl Foo for u8 {
fn method(&self) -> String { format!("u8: {}", *self) }
}

impl Foo for String {
fn method(&self) -> String { format!("string: {}", *self) }
}

// implement below with generics
fn static_dispatch<T: Foo>(x: T) {
x.method();
}

// implement below with trait objects
fn dynamic_dispatch(x: &dyn Foo) {
x.method();
}

fn main() {
let x = 5u8;
let y = "Hello".to_string();

static_dispatch(x);
dynamic_dispatch(&y);

println!("Success!")
}

使用 trait bounds 来修复 largest 函数

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];

for &item in list {
if item > largest {
largest = item;
}
}

largest
}

fn main() {
let number_list = vec![34, 50, 25, 100, 65];

let result = largest(&number_list);
println!("The largest number is {}", result);

let char_list = vec!['y', 'm', 'a', 'q'];

let result = largest(&char_list);
println!("The largest char is {}", result);
}

如果并不希望限制 largest 函数只能用于实现了 Copy trait 的类型,我们可以在 T 的 trait bounds 中指定 Clone 而不是 Copy。并克隆 slice 的每一个值使得 largest 函数拥有其所有权。使用 clone 函数意味着对于类似 String 这样拥有堆上数据的类型,会潜在的分配更多堆上空间,而堆分配在涉及大量数据时可能会相当缓慢。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn largest<T: PartialOrd + Clone>(list: &[T]) -> T {
let mut largest = list[0].clone();

for item in list {
if item > &largest {
largest = item.clone();
}
}

largest
}

fn main() {
let str_list = vec![String::from("hello"),String::from("world")];
let result = largest(&str_list)
println!("The largest word is {}", result);
}

或者 largest 直接返回一个引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn largest<T: PartialOrd + Clone>(list: &[T]) -> &T {
let mut largest = &list[0];

for item in list {
if item > &largest {
largest = item;
}
}

largest
}

fn main() {
let str_list = vec![String::from("hello"),String::from("world")];
let result = largest(&str_list)
println!("The largest word is {}", result);
}

使用 trait bound 有条件地实现方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::fmt::Display;

struct Pair<T> {
x: T,
y: T,
}

impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}

impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}

为满足 trait bound 的所有类型上实现 trait 叫做覆盖实现(blanket implementations)

例如:

1
2
3
impl<T: Display> ToString for T {
// --snip--
}

因为标准库有了这些 blanket implementation,我们可以对任何实现了 Display trait 的类型调用由 ToString 定义的 to_string 方法。例如,可以将整型转换为对应的 String 值,因为整型实现了 Display:

let s = 3.to_string();

Derive 宏派生实现

我们可以使用 #[derive] 属性来派生一些特征,对于这些特征编译器会自动进行默认实现,对于日常代码开发而言,这是非常方便的,例如大家经常用到的 Debug 特征,就是直接通过派生来获取默认实现,而无需我们手动去完成这个工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// `Centimeters`, 一个元组结构体,可以被比较大小
#[derive(PartialEq, PartialOrd)]
struct Centimeters(f64);

// `Inches`, 一个元组结构体可以被打印
#[derive(Debug)]
struct Inches(i32);

impl Inches {
fn to_centimeters(&self) -> Centimeters {
let &Inches(inches) = self;

Centimeters(inches as f64 * 2.54)
}
}

// 添加一些属性让代码工作
// 不要修改其它代码!
#[derive(Debug,PartialEq,PartialOrd)]
struct Seconds(i32);

fn main() {
let _one_second = Seconds(1);

println!("One second looks like: {:?}", _one_second);
let _this_is_true = _one_second == _one_second;
let _this_is_true = _one_second > _one_second;

let foot = Inches(12);

println!("One foot equals {:?}", foot);

let meter = Centimeters(100.0);

let cmp =
if foot.to_centimeters() < meter {
"smaller"
} else {
"bigger"
};

println!("One foot is {} than one meter.", cmp);
}

生命周期

  • Rust 的每个引用都有自己的生命周期
  • 生命周期:引用保持有效的作用域
  • 大多数情况下:生命周期是隐式的,可被推断的
  • 当引用的生命周期可能以不同的方式互相关联时:手动标注生命周期

生命周期存在的目标是:避免悬空引用

尝试使用离开作用域的值的引用会失败

1
2
3
4
5
6
7
8
9
10
{
let r;

{
let x = 5;
r = &x;
}

println!("r: {}", r);
}

借用检查器

Rust 编译器有一个 借用检查器borrow checker),它比较作用域来确保所有的借用都是有效的。

1
2
3
4
5
6
7
8
9
10
{
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
}

这里将 r 的生命周期标记为 ‘a 并将 x 的生命周期标记为 ‘b。如你所见,内部的 ‘b 块要比外部的生命周期 ‘a 小得多。在编译时,Rust 比较这两个生命周期的大小,并发现 r 拥有生命周期 ‘a,不过它引用了一个拥有生命周期 ‘b 的对象。程序被拒绝编译,因为生命周期 ‘b 比生命周期 ‘a 要小:被引用的对象比它的引用者存在的时间更短。

函数中的泛型生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";

let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}

执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
error[E0106]: missing lifetime specifier
--> src\main.rs:1:33
|
1 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `demo` due to previous error

标识生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";

let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}

表示返回值的生命周期是传入参数的两个引用的生命周期的重叠部分。

生命周期标注语法

  • 生命周期的标注不会改变引用的生命周期的长度
  • 当制定了泛型生命周期参数,函数可以接受带有任何生命周期的引用
  • 生命周期的标注:描述了多个引用的生命周期的关系,但不影响生命周期
  • 生命周期的参数名: 以 ‘ 开头,通常全小写且非常短,很多人使用 ‘a

  • 生命周期标注的位置

在引用的&符号后,使用空格将标注和引用类型分开

1
2
3
&i32        // 引用
&'a i32 // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用

泛型生命周期参数声明在:函数名和参数列表之间的<>里

fn longest<’a>(x: &’a str, y: &’a str) -> &’a str {

1
2
3
4
5
6
7
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}

它的实际含义是 longest 函数返回的引用的生命周期与传入该函数的引用的生命周期的较小者一致。

记住通过在函数签名中指定生命周期参数时,我们并没有改变任何传入值或返回值的生命周期,而是指出任何不满足这个约束条件的值都将被借用检查器拒绝。注意 longest 函数并不需要知道 x 和 y 具体会存在多久,而只需要知道有某个可以被 ‘a 替代的作用域将会满足这个签名。

当具体的引用被传递给 longest 时,被 ‘a 所替代的具体生命周期是 x 的作用域与 y 的作用域相重叠的那一部分。换一种说法就是泛型生命周期 ‘a 的具体生命周期等同于 x 和 y 的生命周期中较小的那一个。因为我们用相同的生命周期参数 ‘a 标注了返回的引用值,所以返回的引用值就能保证在 x 和 y 中较短的那个生命周期结束之前保持有效。

使用

1
2
3
4
5
6
7
8
9
fn main() {
let string1 = String::from("long string is long");

{
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("The longest string is {}", result);
}
}

在这个例子中,string1 直到外部作用域结束都是有效的,string2 则在内部作用域中是有效的,而 result 则引用了一些直到内部作用域结束都是有效的值。借用检查器认可这些代码;它能够编译和运行,并打印出 The longest string is long string is long。

修改

1
2
3
4
5
6
7
8
9
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {}", result);
}

程序运行出错

错误表明为了保证 println! 中的 result 是有效的,string2 需要直到外部作用域结束都是有效的。Rust 知道这些是因为(longest)函数的参数和返回值都使用了相同的生命周期参数 ‘a。

深入理解声明周期

  • 指定生命周期参数的正确方式依赖函数实现的具体功能

如果将 longest 函数的实现修改为总是返回第一个参数而不是最长的字符串 slice,就不需要为参数 y 指定一个生命周期。如下代码将能够编译:

文件名: src/main.rs

1
2
3
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}
  • 当从函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配。

如果返回的引用 没有 指向任何一个参数,那么唯一的可能就是它指向一个函数内部创建的值,它将会是一个悬垂引用,因为它将会在函数结束时离开作用域。

1
2
3
4
fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str()
}

编译报错

综上,生命周期语法是用于将函数的多个参数与其返回值的生命周期进行关联的。一旦他们形成了某种关联,Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。

结构体定义中的生命周期注解

struct 里可包括:

  • 自持有类型
  • 引用:需要在每个引用上添加声明周期标注

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
struct ImportantExcerpt<'a> {
part: &'a str,
}

fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
}

表示结构体的生命周期和其成员 part 的生命周期相同。

生命周期的省略

Lifetime Elision

在 Rust 引用分析中所编入的模式称为生命周期省略规则lifetime elision rules

这并不是需要程序员遵守的规则;这些规则是一系列特定的场景,此时编译器会考虑,如果代码符合这些场景,就无需明确指定生命周期。

函数或方法的参数的生命周期被称为 输入生命周期input lifetimes),而返回值的生命周期被称为 输出生命周期output lifetimes)。

编译器采用三条规则来判断引用何时不需要明确的注解。第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。这些规则适用于 fn 定义,以及 impl

  • 规则 1每个引用类型的参数都有自己的声明周期
  • 规则 2如果只有 1 个输入生命周期参数,那么该生命周期被赋给所有的输出生命周期参数
  • 规则 3如果有多个输入生命周期参数,但其中一个是&self 或&mut self(是方法),那么 self 的生命周期就会被赋给所有的输出生命周期参数

例子:

假设我们自己就是编译器。

1
fn first_word(s: &str) -> &str {

接着编译器应用第一条规则,也就是每个引用参数都有其自己的生命周期

1
fn first_word<'a>(s: &'a str) -> &str {

对于第二条规则,因为这里正好只有一个输入生命周期参数所以是适用的。第二条规则表明输入参数的生命周期将被赋予输出生命周期参数,所以现在签名看起来像这样:

1
fn first_word<'a>(s: &'a str) -> &'a str {

现在这个函数签名中的所有引用都有了生命周期,如此编译器可以继续它的分析而无须程序员标记这个函数签名中的生命周期。

1
fn longest(x:&str,y:&str)->&str{

应用第一条规则

1
fn longest<'a,'b>(x:&'a str,y:&'b str)->&str{

第二条规则不适用,第三条不适用,所以编译器报错

方法定义中的生命周期标注

当为带有生命周期的结构体实现方法时,其语法依然类似泛型类型参数的语法。

在哪里声明生命周期参数,依赖于:

  • 生命周期参数是否同结构体字段或方法参数和返回值相关

struct 字段的生命周期名:

  • 在 impl 后声明
  • 在 struct 名后使用
  • 这些生命周期是 struct 类型的一部分

impl 块内的方法签名中:

  • 引用必须绑定于 struct 字段引用的声明周期,或者引用是独立的也可以
  • 生命周期省略规则经常使得方法中的生命周期标注不是必须的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct ImportantExcerpt<'a>{
part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
//这里是一个适用于第三条生命周期省略规则的例子
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}

fn main(){
let novel = String::from("Call me Ishmael.Some year ago...");
let first_sentence = novel.split('.').next().expect("Could not found a '.'");
let i = ImportantExcerpt{
part: first_sentence,
};
}

announce_and_return_part 这里有两个输入生命周期,所以 Rust 应用第一条生命周期省略规则并给予 &self 和 announcement 他们各自的生命周期。接着,因为其中一个参数是 &self,返回值类型被赋予了 &self 的生命周期,这样所有的生命周期都被计算出来了。

静态生命周期

‘static,其生命周期能够存活于整个程序期间。

所有的字符串字面值都拥有 ‘static 生命周期

let s: &’static str = “I have a static lifetime.”;

这个字符串的文本被直接储存在程序的二进制文件中而这个文件总是可用的。因此所有的字符串字面值都是 ‘static 的。

结合泛型类型参数、trait bounds 和生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}

编写自动化测试

Rust 中的测试函数是用来验证非测试代码是否是按照期望的方式运行的。测试函数体通常执行如下三种操作:

  1. 设置任何所需的数据或状态(Arrange)
  2. 运行需要测试的代码(Act)
  3. 断言(Assert)其结果是我们所期望的

测试函数剖析

编写测试函数

Rust 中的测试就是一个带有 test 属性注解的函数。属性(attribute)是关于 Rust 代码片段的元数据

为了将一个函数变成测试函数,需要在 fn 行之前加上 #[test]

运行测试

  • 使用 cargo test 命令运行所有测试函数

Rust Hui 构建一个 Test Runner 的可执行文件,它会运行标注了 test 的函数,并报告其运行是否成功

  • 当使用 cargo 创建 library 项目时,会生成一个 test module,里面有一个 test 函数

可以添加任意数量的 test module 或函数

1
cargo new 项目名 --lib

src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pub fn add(left: usize, right: usize) -> usize {
left + right
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}

运行

1
cargo run test

测试失败

  • 测试函数 panic 就表示失败
  • 每个测试运行在一个新线程
  • 当主线程看见某个测试线程挂掉了,那个测试标记为失败了

断言(Assert)

使用 assert!宏检查测试结果

assert!宏,来自标准库,用来确定某个状态是否为 true

  • true,测试通过
  • false, 调用 panic!,测试失败
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};

assert!(larger.can_hold(&smaller));
}
}

使用 assert_eq! 和 assert_ne! 宏来测试相等

  • 都来自于标准库
  • 判断两个参数是否相等或不等
  • 实际上,它们使用的就是==和!=运算符的 assert!
  • 断言失败,会自动打印出两个参数的值

使用 debug 格式打印参数:要求参数实现了 PartialEq 和 Debug Traits(所有基本类型和标准库大部分类型都实现了)

1
2
3
4
5
6
7
8
9
10
11
12
13
pub fn add_two(a: i32) -> i32 {
a + 2
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_adds_two() {
assert_eq!(4, add_two(2));
}
}

自定义错误信息

可以向 assert!、assert_eq! 和 assert_ne! 宏传递一个可选的失败信息参数,可以在测试失败时将自定义失败信息一同打印出来。

  • assert! 宏的第一个参数必填,自定义消息作为第二个参数
  • assert_eq!和 assert_ne! 前两个参数必填,自定义消息作为第 3 个参数
  • 自定义消息参数会被传递给 format!宏,可以使用{}占位符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
pub fn greeting(name: &str) -> String {
format!("Hello {}!", name)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(result.contains("Carol"));
}
}

自定义错误信息

1
2
3
4
5
6
7
8
9
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(
result.contains("Carol"),
"Greeting did not contain name, value was `{}`",
result
);
}

验证错误处理的情况

可验证代码在特定情况下是否发生了 panic

should_panic 属性(attribute):

  • 函数 panic:测试通过
  • 函数没有 panic:测试失败
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pub struct Guess {
value: i32,
}

impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {}.", value);
}

Guess { value }
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
}

让 should_panic 更精确

可以给 should_panic 属性增加一个可选的 expected 参数。测试工具会确保错误信息中包含其提供的文本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// --snip--
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!(
"Guess value must be greater than or equal to 1, got {}.",
value
);
} else if value > 100 {
panic!(
"Guess value must be less than or equal to 100, got {}.",
value
);
}

Guess { value }
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
#[should_panic(expected = "Guess value must be less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}

在测试中使用 Result

无需 panic,可以使用 Result作为返回类型编写测试

  • 返回 Ok:测试通过
  • 返回 Err:测试失败
1
2
3
4
5
6
7
8
9
10
11
#[cfg(test)]
mod tests {
#[test]
fn it_works() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}
}

注意:不能对这些使用 Result 的测试使用 #[should_panic] 注解

因为运行失败时会返回 Err 而不会发生 panic

控制测试运行

改变 cargo test 的行为:添加命令行参数

默认行为:

  • 并行运行
  • 所有测试
  • 捕获(不显示)所有标准输出,使读取与测试结果相关的输出更容易

命令行参数:

  • 针对 cargo test 的参数: 紧跟 cargo test 后

cargo test —help

  • 针对测试可执行程序: 放在—之后

cargo test — —help

并行运行测试

默认使用多个线程并行运行

要确保测试之间:

  • 不会相互依赖
  • 不依赖于某个共享状态(环境,工作目录,环境变量等)

—test-threads 参数

如果你不希望测试并行运行,或者想要更加精确的控制线程的数量,可以传递 —test-threads 参数和希望使用线程的数量给测试二进制文件。例如:

1
cargo test -- --test-threads=1

这里将测试线程设置为 1,告诉程序不要使用任何并行机制。这也会比并行运行花费更多时间,不过在有共享的状态时,测试就不会潜在的相互干扰了。

显式函数输出

默认,如果测试通过,Rust 的 test 库会捕获所有打印到标准输出的内容

比如 println!:

  • 如果测试成功,我们将不会在终端看到 println! 的输出:只会看到说明测试通过的提示行。
  • 如果测试失败了,则会看到所有标准输出和其他错误信息

如果你希望也能看到通过的测试中打印的值,也可以在结尾加上 —show-output 告诉 Rust 显示成功测试的输出。

1
cargo test -- --show-output

按名称运行测试的子集

如果没有传递任何参数就运行测试,所有测试都会并行运行:

运行单个测试

可以向 cargo test 传递任意测试的名称来只运行这个测试:

1
cargo test one_hundred

过滤运行多个测试

我们可以指定部分测试的名称,任何名称匹配这个名称的测试会被运行。例如,因为头两个测试的名称包含 add,可以通过 cargo test add 来运行这两个测试:

1
2
3
4
5
6
7
8
9
10
$ cargo test add
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.61s
Running unittests (target/debug/deps/adder-92948b65e88960b4)

running 2 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s

这运行了所有名字中带有 add 的测试,也过滤掉了名为 one_hundred 的测试。

忽略某些测试

有时一些特定的测试执行起来是非常耗费时间的,所以在大多数运行 cargo test 的时候希望能排除他们

可以使用 ignore 属性来标记耗时的测试并排除他们,如下所示:

文件名: src/lib.rs

1
2
3
4
5
6
7
8
9
10
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}

#[test]
#[ignore]
fn expensive_test() {
// 需要运行一个小时的代码
}

对于想要排除的测试,我们在 #[test] 之后增加了 #[ignore] 行。现在如果运行测试,就会发现 it_works 运行了,而 expensive_test 没有运行:

如果我们只希望运行被忽略的测试,可以使用 cargo test — —ignored

1
$ cargo test -- --ignored

测试组织

Rust 社区倾向于根据测试的两个主要分类来考虑问题:

单元测试unit tests)与 集成测试integration tests

  • 元测试倾向于更小而更集中,在隔离的环境中一次测试一个模块,或者是测试私有接口。
  • 而集成测试对于你的库来说则完全是外部的。它们与其他外部代码一样,通过相同的方式使用你的代码,只测试公有接口而且每个测试都有可能会测试多个模块。

单元测试

#[cfg(test)]标注 test 模块

  • 只有运行 cargo test 才编译和运行代码
  • 运行 cargo build 则不会
  • 集成测试在不同的目录,它不需要#[cfg(test)]标注

cfg: configuration 配置

告诉 Rust 下面的条目只有在指定的配置选项下才被包含

测试私有函数

测试社区中一直存在关于是否应该对私有函数直接进行测试的论战,而在其他语言中想要测试私有函数是一件困难的,甚至是不可能的事。不过无论你坚持哪种测试意识形态,Rust 的私有性规则确实允许你测试私有函数

文件名: src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pub fn add_two(a: i32) -> i32 {
internal_adder(a, 2)
}

fn internal_adder(a: i32, b: i32) -> i32 {
a + b
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn internal() {
assert_eq!(4, internal_adder(2, 2));
}
}

示例 11-12:测试私有函数

注意 internal_adder 函数并没有标记为 pub。同时 tests 也仅仅是另一个模块。正如 “路径用于引用模块树中的项”部分所说,子模块的项可以使用其上级模块的项。在测试中,我们通过 use super::* 将 test 模块的父模块的所有项引入了作用域,接着测试调用了 internal_adder。

集成测试

在 Rust 里,集成测试完全位于测试库的外部

同其他使用库的代码一样使用库文件,也就是说它们只能调用一部分库中的公有 API 。集成测试的目的是测试库的多个部分能否一起正常工作。一些单独能正确运行的代码单元集成在一起也可能会出现问题,所以集成测试的覆盖率也是很重要的。

tests 目录

  • 创建集成测试:tests 目录
  • tests 目录下的每个测试文件都是单独的一个 crate

创建一个集成测试。保留示例 adder 中 src/lib.rs 的代码。创建一个 tests 目录,新建一个文件 tests/integration_test.rs,并输入示例中的代码。

1
2
3
4
5
6
use adder;

#[test]
fn it_adds_two() {
assert_eq!(4, adder::add_two(2));
}
  • 并不需要将 tests/integration_test.rs 中的任何代码标注为 #[cfg(test)]。 tests 文件夹在 Cargo 中是一个特殊的文件夹, Cargo 只会在运行 cargo test 时编译这个目录中的文件。
  • 需要将被测试库导入

运行指定的集成测试

运行一个特定的集成测试

1
$ cargo test 函数名

运行某个测试文件内的所有测试

1
$ cargo test --test 文件名

集成测试中的子模块

随着集成测试的增加,你可能希望在 tests 目录增加更多文件以便更好的组织他们,例如根据测试的功能来将测试分组。正如我们之前提到的,每一个 tests 目录中的文件都被编译为单独的 crate。

将每个集成测试文件当作其自己的 crate 来对待,这更有助于创建单独的作用域,这种单独的作用域能提供更类似与最终使用者使用 crate 的环境。

例如,如果我们可以创建 一个tests/common.rs 文件并创建一个名叫 setup 的函数,我们希望这个函数能被多个测试文件的测试函数调用:

文件名: tests/common.rs

1
2
3
pub fn setup() {
// setup code specific to your library's tests would go here
}

如果再次运行测试,将会在测试结果中看到一个新的对应 common.rs 文件的测试结果部分,即便这个文件并没有包含任何测试函数,也没有任何地方调用了 setup 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.89s
Running unittests (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::internal ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Running tests/common.rs (target/debug/deps/common-92948b65e88960b4)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4)

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

我们并不想要 common 出现在测试结果中显示 running 0 tests 。我们只是希望其能被其他多个集成测试文件中调用罢了。

为了不让 common 出现在测试输出中,我们将创建 tests/common/mod.rs而不是创建 tests/common.rs 。这是一种 Rust 的命名规范,这样命名告诉 Rust 不要将 common 看作一个集成测试文件。将 setup 函数代码移动到 tests/common/mod.rs 并删除 tests/common.rs 文件之后,测试输出中将不会出现这一部分。tests 目录中的子目录不会被作为单独的 crate 编译或作为一个测试结果部分出现在测试输出中

一旦拥有了 tests/common/mod.rs,就可以将其作为模块以便在任何集成测试文件中使用。这里是一个 tests/integration_test.rs 中调用 setup 函数的 it_adds_two 测试的例子:

文件名: tests/integration_test.rs

1
2
3
4
5
6
7
8
9
use adder;

mod common;

#[test]
fn it_adds_two() {
common::setup();
assert_eq!(4, adder::add_two(2));
}//接着在测试函数中就可以调用 `common::setup()` 了。

针对 binary crate 的集成测试

  • 如果项目时 binary crate 只有含有 src/main.rs 没有 src/lib.rs:

    • 不能在 tests 目录下创建集成测试

    • 无法把 main.rs 的函数导入作用域

  • 只有 library crate 在能暴露函数给其它 crate 使用

  • binary crate 意味着独立运行

IO 项目:构建命令行程序

接收命令行参数

1
2
3
4
5
6
use std::env;

fn main() {
let args: Vec<String> = env::args().collect();
println!("{:?}", args);
}

注意:

std::env::args 在其任何参数包含无效 Unicode 字符时会 panic。

如果你需要接受包含无效 Unicode 字符的参数,使用 std::env::args_os 代替。这个函数返回 OsString 值而不是 String 值。这里出于简单考虑使用了 std::env::args,因为 OsString 值每个平台都不一样而且比 String 值处理起来更为复杂。

二进制项目的关注与分离

main 函数负责多个任务的组织问题在许多二进制项目中很常见。所以 Rust 社区开发出一类在 main 函数开始变得庞大时进行二进制程序的关注分离的指导性过程。这些过程有如下步骤:

  • 将程序拆分成 main.rslib.rs 并将程序的逻辑放入 lib.rs
  • 当命令行解析逻辑比较小时,可以保留在 main.rs 中。
  • 当命令行解析开始变得复杂时,也同样将其从 main.rs 提取到 lib.rs 中。

经过这些过程之后保留在 main 函数中的责任应该被限制为:

  • 使用参数值调用命令行解析逻辑
  • 设置任何其他的配置
  • 调用 lib.rs 中的 run 函数
  • 如果 run 返回错误,则处理这个错误

这个模式的一切就是为了关注分离:main.rs 处理程序运行,而 lib.rs 处理所有的真正的任务逻辑。因为不能直接测试 main 函数,这个结构通过将所有的程序逻辑移动到 lib.rs 的函数中使得我们可以测试他们。仅仅保留在 main.rs 中的代码将足够小以便阅读就可以验证其正确性。

TDD 测试驱动开发

遵循测试驱动开发(Test Driven Development, TDD)的模式来逐步增加 minigrep 的搜索逻辑。这是一个软件开发技术,它遵循如下步骤:

  1. 编写一个失败的测试,并运行它以确保它失败的原因是你所期望的。
  2. 编写或修改足够的代码来使新的测试通过。
  3. 重构刚刚增加或修改的代码,并确保测试仍然能通过。
  4. 从步骤 1 开始重复!

这只是众多编写软件的方法之一,不过 TDD 有助于驱动代码的设计。在编写能使测试通过的代码之前编写测试有助于在开发过程中保持高测试覆盖率。

编写 minigrep 代码

src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
use std::env;
use std::error::Error;
use std::fs;

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
//读取文件
let contents = fs::read_to_string(&config.filename)?;
// println!("With text:\n{}", contents);
let results = if config.case_sensitive {
search(&config.query, &contents)
} else {
search_case_insensitive(&config.query, &contents)
};
for line in results {
println!("{}", line);
}
Ok(())
}

pub struct Config {
pub query: String,
pub filename: String,
pub case_sensitive: bool,
}

impl Config {
pub fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let filename = args[2].clone();
// println!("Search for {}", query);
// println!("In file {}", filename);
let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
Ok(Config {
query,
filename,
case_sensitive,
})
}
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut result = Vec::new();
for line in contents.lines() {
if line.contains(query) {
result.push(line);
}
}

result
}
pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut result = Vec::new();
let query = query.to_lowercase();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
result.push(line);
}
}

result
}

#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe,fast,productive.
Pick three.";
assert_eq!(vec!["safe,fast,productive."], search(query, contents));
}

#[test]
fn case_insensitive() {
let query = "duct";
let contents = "\
Rust:
safe,fast,productive.
Pick three.";
assert_eq!(
vec!["safe,fast,productive."],
search_case_insensitive(query, contents)
);
}
}

src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use std::env;
use std::process;
use minigrep::Config;

fn main() {
let args: Vec<String> = env::args().collect();
// println!("{:?}",args);
let config = Config::new(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments:{}", err);
process::exit(1);
});
if let Err(e) = minigrep::run(config){
eprintln!("Application error: {}",e);
process::exit(1);
};
}