学习资源
Rust 程序设计语言 简体中文版
https://kaisery.github.io/trpl-zh-cn/
安装
https://rust-lang.org/tools/install/
要检查 Rust 是否安装正确,打开 shell 并输入:
$ rustc --version更新与卸载
通过 rustup 安装 Rust 之后,更新到新发布的版本很简单。只需要在 shell 中运行下面的更新脚本:
$ rustup update若要卸载 Rust 和 rustup,请在 shell 中运行下面的卸载脚本:
$ rustup self uninstallVS Code 插件
rust-analyzer:它会实时编译和分析你的 Rust 代码,提示代码中的错误,并对类型进行标注。你也可以使用官方的 Rust 插件取代。rust syntax:为代码提供语法高亮。crates:帮助你分析当前项目的依赖是否是最新的版本。Even Better TOML:Rust 使用 toml 做项目的配置管理。better toml 可以帮你语法高亮,并展示 toml 文件中的错误。rust test lens:可以帮你快速运行某个 Rust 测试。
关闭代码类型灰色提示:
{
"rust-analyzer.inlayHints.typeHints.enable": false,
"rust-analyzer.inlayHints.parameterHints.enable": false,
"rust-analyzer.inlayHints.chainingHints.enable": false
}工具简介
cargo
Rust 的包管理器,构建工具和依赖解决器。可以使用 cargo 命令创建、编辑和构建 Rust 项目
cargo new --bin my_project可以创建一个名为 my_project 的新的 Rust 项目
rustup
用来升级维护 Rust 编译器套件的版本同时支持维护多个版本,并可用来安装 Rust 组件
rustup update stable可将 Rust stable 版本升级至最新
rust-fmt
可用来对 Rust 代码按配置格式进行自动排版,用来统一 Rust 代码风格
配合 cargo,直接在工程目录下运行 cargo fmt 就可以对整个工程进行排版
rust-clippy
可用来对 Rust 代码进行严谨性检查指出一些写得不规范的地方
直接在工程目录下运行 cargo clippy 就可以对整个工程进行排版
工程模块构建
src
- lib.rs
- abi
- xxx.rs
- mod.rssrc/lib.rs 内容
mod abi;src/abi/mod.rs 内容
mode xxx在 Rust 里,一个项目也被称为一个 crate。
crate 可以是可执行项目,也可以是一个库,可以用 cargo new <name> -- lib 来创建一个库。
当 crate 里的代码改变时,这个 crate 需要被重新编译。
在一个 crate 下,除了项目的源代码,单元测试和集成测试的代码也会放在 crate 里。
Rust 的单元测试一般放在和被测代码相同的文件中,使用条件编译 #[cfg(test)] 来确保测试代码只在测试环境下编译。
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}基础命令
- 可以使用
cargo new创建项目。 - 可以使用
cargo build构建项目。 - 可以使用
cargo run一步构建并运行项目。 - 可以使用
cargo check在不生成二进制文件的情况下构建项目来检查错误。 - 有别于将构建结果放在与源码相同的目录,Cargo 会将其放到 target/debug 目录
所有权
Rust明确了所有权的概念,值也可以叫资源,所有权就是拥有资源的权利。一个变量拥有一个资源的所有权,那它就要负责那个资源的回收、释放。 Rust基于所有权定义出发,推导出了整个世界。
所有权的基础是三条定义。
Rust中,每一个值都有一个所有者。
任何一个时刻,一个值只有一个所有者。
当所有者所在作用域(scope)结束的时候,其管理的值会被一起释放掉。
这三条规则涉及两个概念: 所有者和作用域。
所谓所有者,在代码里就用变量表示。而变量的作用域,就是变量有效(valid)的那个代码区间。在Rust中,一个所有权型变量的作用域,简单来说就是它定义时所在的那个最里层的花括号括起的部分,从变量创建时开始,到花括号结束的地方。
堆内存资源随着关联的栈上局部变量一起被回收 的内存管理特性,叫作 RAII(Resource Acquisition Is Initialization)
移动和复制所有权
u32这种类型在做变量的再赋值的时候,是做了复制所有权的操作。而String这种类型在做变量再赋值的时候,是做了移动所有权的操作。
默认做复制所有权的操作的有7种。
所有的整数类型,比如u32;
布尔类型bool;
浮点数类型,比如f32、f64;
字符类型char;
由以上类型组成的元组类型 tuple,如(i32, i32, char);
由以上类型组成的数组类型 array,如 [9; 100];
不可变引用类型&。
其他类型默认都是做移动所有权的操作。
引用
引用分成不可变引用和可变引用。
&x是对变量x的不可变引用。&mut x是对变量x的可变引用一个所有权型变量的作用域是从它定义时开始到花括号结束。而引用型变量的作用域不是这样, 引用型变量的作用域是从它定义起到它最后一次使用时结束,定义引用,但并没有被使用,它的作用域就只有那一行
一个所有权型变量的可变引用与不可变引用的作用域不能交叠,也可以说不能同时存在
引用(不可变引用和可变引用)型变量的作用域不会长于所有权变量的作用域。这是肯定的,不然就会出现悬锤引用,这是典型的内存安全问题。
一个所有权型变量的不可变引用可以同时存在多个,可以复制多份。
某个时刻对某个所有权型变量只能存在一个可变引用,不能有超过一个可变借用同时存在,也可以说,对同一个所有权型变量的可变借用之间的作用域不能交叠。
在有借用存在的情况下,不能通过原所有权型变量对值进行更新。当借用完成后(借用的作用域结束后),物归原主,又可以使用所有权型变量对值做更新操作了。
多级引用
只有全是多级可变引用的情况下,才能修改到目标资源的值。
对于多级引用(包含可变和不可变),打印语句中,可以自动为我们解引用正确的层数,直到访问到目标资源的值,这很符合人的直觉和业务的需求。
变量
let x = 5;
let mut x = 5;
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;数据类型
整型
| 长度 | 有符号 | 无符号 |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| 架构相关 | isize | usize |
整型字面值
| 数字字面值 | 例子 |
|---|---|
| Decimal(十进制) | 98_222 |
| Hex(十六进制) | 0xff |
| Octal(八进制) | 0o77 |
| Binary(二进制) | 0b1111_0000 |
Byte(字节字面值,仅限 u8) | b'A' |
浮点型
Rust 的浮点数类型是 f32 和 f64,分别占 32 位和 64 位。默认类型是 f64,因为在现代 CPU 中,它与 f32 速度几乎一样,不过精度更高。所有的浮点型都是有符号的。
布尔类型
true和false
字符类型
单引号来表示 char 字面值,而字符串字面值使用的是双引号。Rust 的 char 类型大小为 4 个字节,并表示一个 Unicode 标量值(Unicode Scalar Value),这意味着它所能表示的内容远不止 ASCII。带重音符号的字母,中文、日文、韩文字符,emoji,以及零宽空格,都是 Rust 中合法的 char 值。Unicode 标量值的范围包括 U+0000 到 U+D7FF,以及 U+E000 到 U+10FFFF。
复合类型
元组类型
元组是一种将多个不同类型的值组合成一个复合类型的通用方式。元组长度固定:一旦声明,它的大小就不能增长或缩小。
我们通过在圆括号中写一组由逗号分隔的值来创建元组。元组中的每个位置都有一个类型,而且这些不同位置上的值类型不必相同。
可以使用模式匹配(pattern matching)来解构(destructure)元组
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {y}");
}也可以使用点号(.)后跟值的索引来直接访问所需的元组元素
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)。这种值以及其对应的类型都写作 (),表示空值或空的返回类型。如果一个表达式没有返回任何其他值,它就会隐式返回单元值。
数组类型
数组的值写成在方括号内,用逗号分隔的列表:
fn main() {
let a = [1, 2, 3, 4, 5];
}当你希望把数据分配在栈(stack)上而不是堆(heap)上时,或者当你想确保始终拥有固定数量的元素时使用
编写数组的类型:在方括号中包含每个元素的类型,后跟分号,再后跟数组元素的数量。
let a: [i32; 5] = [1, 2, 3, 4, 5];
// 通过在方括号中指定初始值加分号再加元素个数的方式来创建一个每个元素都为相同值的数组:
let a = [3; 5];函数
Rust 代码中的函数名和变量名通常使用 snake case 风格
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Another function.");
}rust 不会给返回值命名,但必须在箭头(->)后面声明它的类型。
在 Rust 中,函数的返回值等同于函数体中最后一个表达式的值。表达式的结尾没有分号。
可以使用 return 关键字并指定一个值,从函数中提前返回;不过大多数函数都会隐式返回最后一个表达式的值。
fn five() -> i32 {
5
}
fn main() {
let x = five();
println!("The value of x is: {x}");
}控制流
if 表达式
Rust 不会自动尝试把非布尔类型转换成布尔类型。你必须显式地为 if 提供一个布尔值作为条件。
fn main() {
let number = 3;
if number != 0 {
println!("number was something other than zero");
}
}在 let 语句中使用 if
因为 if 是一个表达式,我们可以在 let 语句的右侧使用它,if 的各个分支可能产生的结果值都必须是相同类型
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {number}");
}使用循环重复执行
loop
loop 关键字告诉 Rust 反复执行一段代码,要么永远执行下去,要么直到你明确要求它停止。
fn main() {
loop {
println!("again!");
}
}用于停止循环的 break 表达式后面加上想要返回的值;这个值会作为循环的返回值返回出来
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 一起使用,这样这些关键字就会作用于被标记的循环,而不是最内层循环。
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 条件循环
fn main() {
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!!!");
}使用 for 遍历集合
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
}只想把某段代码执行特定次数的情况下,可以使用 range
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}
// 步长为 2
for i in (0..10).step_by(2) {
println!("{}", i); // 0, 2, 4, 6, 8
}
// 从 100 到 0,步长 10
for i in (0..=100).rev().step_by(10) {
println!("{}", i); // 100, 90, 80... 0
}使用迭代器
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()
}字符串
slice
字符串 slice(string slice)是 String 中一部分值的引用,可以使用一个由中括号中的 [starting_index..ending_index] 指定的 range 创建一个 slice
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
let slice = &s[3..];
let slice = &s[..];结构体
定义和实例化
fn main() {
let mut user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");
}字段初始化简写语法
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}.. 语法指定了剩余未显式设置值的字段应有与给定实例对应字段相同的值。
fn main() {
// --snip--
let user2 = User {
email: String::from("another@example.com"),
..user1
};
}元组结构体
也可以定义与元组类似的结构体,称为 元组结构体(tuple structs)。元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}类单元结构体
类单元结构体在你想要在某个类型上实现 trait,但又不需要在该类型本身中存储任何数据时会很有用。
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}通过派生 trait 增加功能
对于结构体,println! 应该用来输出的格式是不明确的,Rust 不会尝试猜测我们的意图,所以结构体并没有提供一个 Display 实现来使用 println! 与 {} 占位符。
Rust 确实 包含了打印出调试信息的功能,不过我们必须为结构体显式选择这个功能。为此,在结构体定义之前加上外部属性 #[derive(Debug)]
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {rect1:?}");
println!("rect1 is {rect1:#?}");
}另一种使用 Debug 格式打印数值的方法是使用 dbg! 宏。dbg! 宏接收一个表达式的所有权(与 println! 宏相反,后者接收的是引用),打印出代码中调用 dbg! 宏时所在的文件和行号,以及该表达式的结果值,并返回该值的所有权。
注意:调用
dbg!宏会打印到标准错误控制台流(stderr),与println!不同,后者会打印到标准输出控制台流(stdout)。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
}
/*
[src/main.rs:10:16] 30 * scale = 60
[src/main.rs:14:5] &rect1 = Rectangle {
width: 60,
height: 50,
}
*/方法
方法与函数是不同的,因为它们在结构体的上下文中被定义(或者是枚举或 trait 对象的上下文,
它们第一个参数总是 self,它代表调用该方法的结构体实例。
语法
使用 &self 来替代 rectangle: &Rectangle,&self 实际上是 self: &Self 的缩写。在一个 impl 块中,Self 类型是 impl 块的类型的别名。
方法的第一个参数必须有一个名为 self 的Self 类型的参数,所以 Rust 让你在第一个参数位置上只用 self 这个名字来简化。
(impl 是 implementation 的缩写)
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}关联函数
所有在 impl 块中定义的函数被称为 关联函数(associated functions)
定义不以 self 为第一参数的关联函数(因此不是方法),因为它们并不作用于一个结构体的实例。
不是方法的关联函数经常被用作返回一个结构体新实例的构造函数。
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
let sq = Rectangle::square(3);多个 impl 块
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}枚举
IpAddrKind 枚举来表现这个概念并列出可能的 IP 地址类型,V4 和 V6。这被称为枚举的变体(variants):
enum IpAddrKind {
V4,
V6,
}
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;如果我们想要将 V4 地址存储为四个 u8 值而 V6 地址仍然表现为一个 String
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));嵌入多种多样的类型
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}Option 枚举
Option 类型应用广泛因为它编码了一个非常普遍的场景,即一个值要么有值要么没值。
Rust 并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是 Option<T>
enum Option<T> {
None,
Some(T),
}match 控制流
match 是极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较,并根据相匹配的模式执行相应代码。模式可由字面值、变量、通配符和许多其他内容构成
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,
}
}匹配 Option<T>
编写一个函数,它获取一个 Option<i32> ,如果其中含有一个值,将其加一。如果其中没有值,函数应该返回 None 值,而不尝试执行任何操作。
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);Rust 中的匹配是 穷尽的(exhaustive):必须穷举到最后的可能性来使代码有效。
通配模式和 _ 占位符
other 这种通配模式满足了 match 必须被穷尽的要求。必须将通配分支放在最后,因为模式是按顺序匹配的。
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other),
}_ ,这是一个特殊的模式,可以匹配任意值而不绑定到该值。这告诉 Rust 我们不会使用这个值,所以 Rust 也不会警告我们存在未使用的变量。
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(),
}if let 和 let else 简洁控制流
if let 语法让我们以一种不那么冗长的方式结合 if 和 let,来处理只匹配一个模式的值而忽略其他模式的情况。
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {max}"),
_ => (),
}
// 简化
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {max}");
}可以在 if let 中包含一个 else。else 块中的代码与 match 表达式中的 _ 分支块中的代码相同,这样的 match 表达式就等同于 if let 和 else
let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {state:?}!");
} else {
count += 1;
}let else
Rust 提供了 let...else。let...else 语法左侧是一个模式,右侧是一个表达式,非常类似于 if let,不过它没有 if 分支,只有 else 分支。
如果模式匹配,它会将匹配到的值绑定到外层作用域。如果模式不匹配,程序流会指向 else 分支,它必须从函数返回。
fn describe_state_quarter(coin: Coin) -> Option<String> {
let state = if let Coin::Quarter(state) = coin {
state
} else {
return None;
};
if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
}
// 改为
fn describe_state_quarter(coin: Coin) -> Option<String> {
let Coin::Quarter(state) = coin else {
return None;
};
if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
}