Rust 学习笔记
The Rust Programming Language 的阅读笔记
1. 入门指南
1.1 安装
Rust 安装见 https://kaisery.github.io/trpl-zh-cn/ch01-01-installation.html 。
更新和卸载
Rust 是可以更新的,不像 Python 那样每个版本都需要重新装。
1
rustup update
卸载 Rust 用如下命令
1
rustup self uninstall
查看离线文档
1
rustup doc
1.2 Hello, World!
rustc
编译文件
创建一个文件 main.rs
,编辑如下内容
1
2
3
fn main() {
println!("Hello rust");
}
在命令行输入
1
rustc main.rs
会生成一个可执行文件,运行该文件即可打印 Hello rust
。
1.3 Hello, Cargo!
使用 rustc
来编译源码,可以,但有点繁琐。Cargo 是 Rust 的构建系统和包管理器,适合复杂项目。
创建项目
1
cargo new hello_cargo --vcs=none
该命令创建一个名为 hello_cargo
的文件夹,里面包含一些必要文件。默认情况下也会添加 git 版本控制,添加 --vcs=none
可以取消。
构建项目
1
2
cd hello_cargo
cargo build
编译项目,默认是 Debug 模式,会在 target/debug
下生成项目同名的可执行文件。
1
cargo run
编译项目之后运行可执行文件,更方便,因此比 cargo build
更常用。
1
cargo check
检查代码是否可编译但不实际编译项目,因此速度比 cargo build
更快。
发布构建
1
cargo build --release
在 target/release
下生成可用于发布的可执行文件。相比于 Debug 模式,编译时间可能更久(因为需要执行一些运行优化),但运行更快(因为有运行优化)。
3. 常见编程概念
3.1 变量与可变性
变量
Rust 的变量用 let
声明,当变量类型可以被推断时,可以不用显式指明类型,当类型不可推断时,需要显式指明类型。
1
2
let a = 1; //可以推断是 i32 类型
let b: i32; //只声明未赋值,不能推断类型,需要指明
Rust 的变量默认都是不可变的,即赋值后便不可更改,要创建可变的变量需要用 let mut
声明。
1
2
let mut c = 3;
c += 2; //赋新值或者自增等都属于改变
常量
Rust 的常量用 const
声明,永不可变,不能加 mut
修饰。
常量 必须 指明类型。
常量值必须在编译时就能确定,不能赋一个运行时才能知道的值,即只能是常量表达式。
1
const a: i32 = 3;
常量可以定义在全局作用域,变量只能定义在函数里。
1
2
3
4
5
6
const a: i32 = 3; //ok
let b = 3; //error
fn main() {
let c = 4; //ok
}
隐藏
Rust 可以用 let
多次声明同一变量,这一点与 JavaScript 不同。
1
2
let x = 3;
let x = x + 4;
这里第一行的 x
依然是不可变的,只不过第二行用 let
重新声明后这个 x
与第一行的 x
就是两个完全不同的变量了,只不过恰好名称相同。这一操作叫隐藏(Shadowing)。
因为是两个不同的变量了,所以类型啥的也没什么要求,下面的代码是可行的
1
2
let x = 3;
let x = "hello";
其中第一行的 x
的类型是 i32
,而第二行的是 &str
。两个变量除了名称相同外没什么联系。
但如下代码是不可行的
1
2
let mut x = 3;
x = "hello";
第一行声明了 x
这一变量为 i32
类型的,虽然是可变的,但这只代表其值可变,类型是不能变的,因此第二行试图赋值一个字符串时会失败。
因为第二行的 x
没有用 let
重新声明,因此它与第一行的 x
是同一个变量,虽然可变,但必须是 i32
类型的。
3.2 数据类型
整型
长度 | 有符号 | 无符号 |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
isize
在 32 位操作系统上是 i32
,在 64 位操作系统上是 i64
,usize
同理。
数字字面值 | 例子 |
---|---|
Decimal (十进制) | 98_222 |
Hex (十六进制) | 0xff |
Octal (八进制) | 0o77 |
Binary (二进制) | 0b1111_0000 |
Byte (单字节字符)(仅限于u8) | b'A' |
数字字面值中可以添加下划线来分组,如 1_000
。
Rust 的整型字面值默认是 i32
类型的,但可以通过添加类型后缀来指明类型
1
2
let a = 24;
let b = 24u8;
变量 a
是 i32
类型的,变量 b
是 u8
类型的。变量 b
的字面值也可以写作 24_u8
。
整型溢出在 Debug 模式下会使程序 panic,在 Release 模式下则不会。
浮点型
Rust 的浮点型有 f32
和 f64
两种。字面值默认是 f64
的。
1
2
3
4
let a = 0.4; //f64
let b: f32 = 0.4;
let c = 0.4f32;
let d = 0.4_f32;
后三个变量都是 f32
类型的。
注意,没有
fsize
这种类型。
Rust 的整数除法与 C 是一样的,不会产生浮点数,而是向 0 舍入到最近的整数。
1
2
let a = 7 / 2; //3
let b = 7.0 / 2.0; //3.0
貌似不能让浮点数与整数进行运算。
布尔型
Rust 的布尔类型是 bool
,只有两个值:true
和 false
。
字符类型
Rust 的字符类型是 char
,大小是 4 个字节,代表一个 Unicode 标量值,因此 Rust 的一个字符可以是任意 Unicode 字符。
元组类型
元组类型,长度固定,元素随意。
1
let a: (i32, f64, char) = (30, 2.4, 'a');
元组可以解构
1
2
3
let t = (30, 2.4, 'a');
let (i, f, c) = t;
println!("i: {i}, f: {f}, c: {c}");
用点和索引可以访问元组元素
1
2
let t = (30, 2.4, 'a');
let i = t.0;
单元(unit)元组是一种特殊值,写作 ()
,类型也是 ()
,表示空。如果一个函数没有显式返回任何值,则会隐式返回 ()
。
数组类型
数组类型,长度 固定,元素类型也必须相同。
1
2
let a: [i32; 3] = [1, 2, 3];
let b: [f64; 3] = [4.0; 3];
第二行是声明 3 个 4.0
的快捷方式。
此处的类型注解不是必须的,只是为了说明数组类型的注解方式。
使用方括号索引数组元素
1
2
let a = [1, 2, 3];
let t = a[0];
3.3 函数
Rust 的函数要求 必须声明每个参数的类型。
1
2
3
4
5
6
7
fn main() {
say_age('B', 34);
}
fn say_age(name: char, age: i32) {
println!("{name} is {age} years old.");
}
语句和表达式
Rust 是一门基于表达式的语言。
表达式 计算并产生一个值,语句 执行一些操作,但不返回值。
let a = 3;
是一个语句,函数定义也是语句。
5 + 6
是一个表达式,5
也是一个表达式。函数调用是表达式,宏调用也是表达式,大括号建立的块作用域也是表达式
1
2
3
4
5
6
7
fn main() {
let y = {
let x = 3;
x + 1
};
println!("y: {y}");
}
注意,上述 x + 1
后没有分号,表示它是一个表达式,也是整个块作用域的返回值。
有返回值的函数
函数的返回值等于函数体最后一个表达式的值。如果使用 return
可以提前返回。
1
2
3
4
5
6
7
8
fn five() -> i32 {
5 //这里没有分号
}
fn main() {
let f = five();
println!("{f}");
}
函数返回值需要声明类型。
3.5 控制流
if
表达式
if
是一个表达式,因此 if
的结果可以返回给一个变量
1
2
3
4
5
6
7
let n = 10;
let a = if n > 4 {
n + 1
} else {
n - 2
};
println!("n: {n}, a: {a}");
注意,n + 1
和 n - 2
后都没有分号,表示它们都是表达式,其结果会赋值给 a
。
形如
1
2
3
4
5
6
7
fn is_even(num: i32) -> bool {
if num % 2 == 0 {
true
} else {
false
}
}
这样的函数也可能常见,一个函数体的最后是一个 if
表达式,每个 if
分支的最后都是一个表达式,某个符合条件的表达式就会作为返回值返回。虽然看上去不是很直观,但的确是这么回事。
if
表达式的判定条件必须是 bool
值,像数字啥的就不行。
三元运算可以像下面这样表达
1
2
let r = 1;
let a = if r == -1 { "No" } else { "Yes" };
if
每个分支中返回的值的类型必须相同。
循环
Rust 有三种循环:loop
、while
、for
。
loop
循环没啥条件,只能用 break
终止。
loop
是一个表达式,因此也可以返回值,break
也是个表达式,可以像用 return
一样使用,可以理解为中断循环时返回一个值。
1
2
3
4
5
6
7
8
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("result: {result}");
当多层循环嵌套时,可以给循环加标签,然后在 break
的时候指明要跳出哪层循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let mut count = 0;
'outer: loop {
println!("count = {count}");
let mut remain = 10;
loop {
println!("remain = {remain}");
if remain == 9 {
break;
}
if count == 2 {
break 'outer;
}
remain -= 1;
}
count += 1;
}
break
把标签和返回值同时使用时返回值需要在后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let mut count = 0;
let res = 'outer: loop {
println!("count = {count}");
let mut remain = 10;
loop {
println!("remain = {remain}");
if remain == 9 {
break;
}
if count == 2 {
break 'outer count;
}
remain -= 1;
}
count += 1;
};
println!("res: {res}");
while
没啥新东西,不说了。
for
循环的话举两个例子就清楚了
1
2
3
4
5
6
7
8
9
let a = [1, 2, 3, 5, 6];
for i in a {
print!("{i} ");
}
print!("\n");
for j in 2..5 {
print!("{j} "); // 2 3 4
}
print!("\n");
其中 2..5
不包括 5
。
4. 认识所有权
这里先简单记一点关于权限的东西,稍后再细节整理:
R
:可读取数据,可拷贝数据
W
:可原地更改数据
O
:可转交所有权,可释放内存
当一个不可变变量创建的时候(包括用 let
声明赋值和初始化函数参数),它拥有 R-O
权限,如果是一个可变的变量,或者可变函数参数,则拥有 RWO
权限。
当一个变量被不可变引用时,失去 -WO
权限,如果被可变引用,则失去 RWO
权限。
一个引用的变量,其解引用的权限,可以根据类型判断,如果是 &T
则解引用后只有 R--
权限,如果是 &mut T
则解引用后有 RW-
权限。因为是引用,所以都没有 --O
权限。
为什么要关心一个引用变量解引用后的权限?因为我们操作一个引用变量的时候,大部分时候都是在操作其解引用后的数据,只不过 Rust 自动隐式地帮我们解引用我们不用直接明写出来,但还是需要考虑的。
详细的内容请查看 Rust 所有权。
5. 使用结构体组织相关联的数据
5.1 结构体的定义和实例化
结构体基本使用
定义结构体
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 {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
}
实例的字段顺序不需要与定义时的一致。
使用结构体的字段
1
println!("name: {}", user1.username);
如果结构体实例是可变的,则可以修改字段
1
2
3
4
5
6
7
8
9
10
11
fn main() {
let mut user1 = User {
active: true,
username: String::from("username123"),
email: String::from("userone@example.com"),
sign_in_count: 1,
};
user1.sign_in_count += 1;
println!("{} has signed in {} times", user1.username, user1.sign_in_count);
}
注意,不能只指定结构体的某个字段可变。要变整个都变。
结构体实例也是表达式,因此可以放在函数最后返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn build_user(username: String, email: String) -> User {
User {
active: true,
username: username,
email: email,
sign_in_count: 1
}
}
fn main() {
let mut user1 = build_user(
String::from("username123"),
String::from("userone@example.com")
);
user1.sign_in_count += 1;
println!("{} has signed in {} times", user1.username, user1.sign_in_count);
}
上述函数中,结构体字段名和函数参数名相同,这种情况也很常见,因此有一种简写语法
1
2
3
4
5
6
7
8
fn build_user(username: String, email: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1
}
}
有时候我们创建的新结构体实例的大部分字段与已有的相同,只有部分不同,就可以用结构体更新语法
1
2
3
4
5
let user2 = User {
email: String::from("another@example.com"),
..user1
};
println!("{} 's email is {}", user2.username, user2.email);
注意,..user1
必须在结构体所有字段的最后,且之后不能有逗号。
..user1
这样的更新语法等同于 =
赋值,因此也可能会转移所有权。比如上例中,user1.username
就被转移了所有权,在创建 user2
之后就不能使用 user1.username
了,但是 user.active
或者 user.sign_in_count
还可以使用,因为它们不是堆数据,支持 Copy。
元组结构体
元组结构体的字段只有类型而没有名称,使用方法类似元组
1
2
3
4
5
6
7
8
9
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let grey = Color(127, 127, 127);
let center = Point(5, 10, 5);
println!("R: {}, y: {}", grey.0, center.1);
}
类单元结构体
类单元结构体就跟单元元组类似,没有任何字段,通常用于定义一个不需要存储任何数据只需要实现 trait 的结构体(之后讨论)
1
2
3
4
5
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}