前言
在当今这个快速发展的软件开发领域,掌握一门高效且安全的编程语言显得尤为重要。Rust,作为一门集性能与安全性于一身的现代系统编程语言,自2010年首次公开以来,便以其独特的所有权系统和零成本抽象理念吸引了大量开发者的关注。本系列文章旨在为初学者提供一条系统学习Rust的道路,从基础语法到高级特性,再到运行机制的深入理解,帮助读者逐步提升,最终达到能够熟练运用Rust解决实际问题的能力。
Rust 学习曲线比较陡峭,理解难度很大,不适合以 Demo 样例为教学,需要像学习 C/C++ 一样从最基础的内容学习,并深刻理解其运转原理,所以这期的内容就是掰开了揉碎了从最基础的教学开始。
本篇作为系列的第七篇文章,我们将继续深入探索Rust的核心概念,通过一系列实例和练习,帮助大家巩固对Rust语法、语言设计以及运行原理的理解。无论你是完全的新手,还是有一定编程经验的开发者,都能从本系列中找到适合自己的学习路径。让我们一起踏上这段从零开始的Rust修炼之旅,共同成长,共同进步。
第一节 变量与不可变性
Rust 中的变量基础知识
在 Rust 中,使用 let 关键字来声明变量
Rust 支持类型推导,但你也可以显式指定变量的类型:
let x: i32 = 5; // 显式指定 x 的类型为 i32
1
2
3
4
5
6
7
8
9
3. 变量名蛇形命名法(Snake Case),而枚举和结构体命名使用帕斯卡命名法(Pascal Case)
1. 蛇形命名法(snake_case)是指**每个空格皆以底线(_)取代的书写风格,且每个单字的第一个字母皆为小写**。
1.
2. ```Rust
fn creat_markdown_file() {}帕斯卡(pascal)与骆驼命名法类似。只不过骆驼命名法是首字母小写,而帕斯卡命名法是首字母大写。
enum ClickEvent {}
1
2
3
4
5
3. 如果变量没有用到,可以前置下划线,消除警告
1. ```Rust
_ = get_loop_count();
强制类型转换 Casting a Value to a Different Type
let a = 3.1; let b = a as i32;
1
2
3
4
5
6
7
8
5. 打印变量({} 与 {:?} 需要实现特质之后章节会介绍,基础类型默认实现)
1. Rust 中 print! 和 println! 是两个常用的宏,用于在控制台输出信息。它们的主要区别在于是否在输出内容后自动添加换行符。
2. ```Rust
println!("val: {}", x);
println!("val: {x}");
Rust 中的变量是默认不可变的
不可变性是 Rust 实现其可靠性和安全性目标的关键。
它迫使程序员更深入地思考程序状态的变化,并明确哪些部分的程序状态可能会发生变化的。
不可变性有助于防止一类常见的错误,如数据竞争和并发问题。
使用 mut 关键字进行可变声明
如果你希望一个变量是可变的,你需要使用 mut 关键字进行明确声明
1 | let mut y = 10; // 可变变量 |
Shadowing Variables 并不是重新赋值
Rust 允许你隐藏一个变量,这意味着你可以声明一个与现有变量同名的新变量,从而有效地隐藏前一个变量
- 可以改变值
- 可以改变类型
- 可以改变可变性
1 | let x = 5; |
不可变性与命名
1 | fn main() { |
第二节 常量 const 与静态变量 static
const 常量
- 常量的值必须是在编译时已知的常量表达式,必须指定类型与值
- 与 C 语言的宏定义(宏替换)不同,Rust 的 const 常量的值被直接嵌入到生成的底层机器代码中,而不是进行简单的字符替换
- 常量名与静态变量命名必须全部大写,单词之间加入下划线
static 静态变量
- 与 const 常量不同,static 变量是在运行时分配内存的
- 并不是不可变的,可以使用 unsafe 修改(少看 Rust 死灵书,尽量不要使用 unsafe)
- 静态变量的生命周期为整个程序的运行时间
1 | static MY_STATIC: i32 = 42; |
第三节 Rust 中的基础数据类型
- Integer Types 默认推断为 i32
- i8、i16、i32、i64、i128
- Unsigned Integer Types
- u8、u16、u32、u64、u128
- Platform-Specific Integer Type(由平台决定,平台指的是运行该代码的设备或环境。具体来说,usize 和 isize 的大小会根据运行代码的平台(如 32 位或 64 位系统)而变化。)
- usize
- 无符号整数类型。
- 大小与当前平台的指针大小相同。
- 32位平台都是32位(4字节),常见于较旧的计算机系统或某些嵌入式设备。
- 64位平台都是64位(8字节),常见于现代计算机系统,包括大多数桌面电脑和服务器
- 通常用于表示内存地址、数组索引或集合的大小。适用于非负数的场景,例如数组索引、集合大小等。例如,获取数组的长度或访问数组中的元素。
- Isize
- 有符号整数类型。
- 大小与当前平台的指针大小相同。
- 32位平台都是32位(4字节),常见于较旧的计算机系统或某些嵌入式设备。
- 64位平台都是64位(8字节),常见于现代计算机系统,包括大多数桌面电脑和服务器
- 通常用于需要负数的情况,例如数组索引的偏移量。例如,在数组中向前或向后移动指针时。
- usize
- Float Types
- f32 与 f64
- 尽量用 f64,除非你清楚边界需要空间
- f32 与 f64
- Boolean Values
- true
- false
- Character Types
- Rust 支持 unicode 字符
- 表示 char 类型使用单引号
1 | fn main() { |
在 Rust 中,emoji_char 是一个 Unicode 字符(char 类型)。当我们将 emoji_char 转换为 usize 或 i32 时,实际上是将这个字符的 Unicode 码点(code point)转换为相应的整数类型。
- Unicode 码点:
- 每个 Unicode 字符都有一个唯一的码点,表示为一个整数值。
- 例如,表情符号 😊 的 Unicode 码点是 U+1F60A。
- 类型转换:
- emoji_char as usize:将 emoji_char 的 Unicode 码点转换为 usize 类型。
- emoji_char as i32:将 emoji_char 的 Unicode 码点转换为 i32 类型。
- 输出相同的原因:
- 在大多数情况下,usize 和 i32 都足够大,可以容纳常见的 Unicode 码点。
- 由于 emoji_char 的 Unicode 码点在 usize 和 i32 的范围内,因此转换后的值是相同的。
第四节 元组与数组
相同点
- 元组和数组都是 Compound Types(复合类型),而 Vec 和 Map 都是 Collection Types(集合类型)
- 复合类型允许你将多个值组合成一个单一的类型。Rust 中主要有三种复合类型:元组(Tuple)数组(Array)和结构体(Struct)。
- 集合类型用于存储多个相同类型的值,并且这些值的数量可以在运行时动态变化。Rust 中主要有三种集合类型:向量/动态数组(Vector)、字符串(String)和哈希映射(HashMap)。
- String 可以简单的理解为 Vector 的一种变形
- 元组和数组的长度都是固定的
- Tuples(元组)不同类型的数据类型
- Arrays(数组)同一类型的数据类型
- 元组和数组均可以设置为可变,但只能改变值的内容,不能改变值的类型
数组
数组是固定长度的同构集合
创建方式
[a, b, c]
[value; size]
/// 第一种创建方法,把所有元素一次性列出来 [a, b, c] /// 第二种创建方法 /// 以你第一个设置的值为初始值,创建一个数组,数组的长度由size定义 [value; size]
1
2
3
4
5
- 获取元素
- ```Rust
arr[index]
获取长度
arr.len()
1
2
3
4
5
6
7
8
9
10
### 元组
- 元组是固定长度的异构集合
- 空元组
- ```Rust
/// 空元组不占任何内存,一般用作默认返回值,比如无返回值时返回一个空元组,实际上等于返回空
Empty Tuple()
元组获取元素
tup.index
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
- 元组无法获取长度,没有len()
### 让我们关注一下 Ownership 所有权机制
#### 类型基础类型与数组、元组他们和 String 数据类型的不同
```Rust
fn main() {
// 元组
let tup = (0, "hi", 3.4);
println!("tup elements {} {} {}", tup.0, tup.1, tup.2);
// 设置元组可变性
let mut tup2 = (0, "hi", 3.4);
println!("tup2 elements {} {} {}", tup2.0, tup2.1, tup2.2);
tup2.1 = "f"; // 注意,可以改变值的内容,但不能改变值的类型
println!("tup2 elements {} {} {}", tup2.0, tup2.1, tup2.2);
// 空元组
let tup3 = ();
/// 打印一下 tup3 的结构
println!("tup3 {:?}", tup3);
// println!("tup3 {}", tup3); // 不能这样,因为没有实现
// 数组
let mut arr = [11, 12, 13];
arr[0] = 999;
println!("arr len {} first elements is {}", arr.len(), arr[0]);
// 遍历数组的几种写法
// 写法1
for element in arr {
println!("{}", element);
}
// 写法2
for (index, &element) in arr.iter().enumerate() {
println!("index = {}, element = {}", index, element);
}
// 写法3,0..arr.len() 表示 [0, arr.len() - 1]
for i in 0..arr.len() {
println!("index = {}, element = {}", i, arr[i]);
}
// 写法4,0..=arr.len() 表示 [0, arr.len()]
for i in 0..=arr.len() {
println!("index = {}", i);
}
let ar = [2; 3];
for i in ar {
println!("{}", i);
}
// ownership 所有权
let arr_item = [1, 2, 3];
let tup_item = (2, "ff");
// 这个打印主要是为了验证是否存在,能否被打印
println!("arr: {:?}", arr_item);
println!("tup: {:?}", tup_item);
let arr_ownership = arr_item;
let tup_ownership = tup_item;
println!("arr: {:?}", arr_item);
println!("tup: {:?}", tup_item);
// 基础数据类型都是可以被打印的
let a = 3;
let b = a;
println!("{a}");
// copy 操作
// 我们前面讲的所有数据类型,它在赋值时执行的都是 copy 操作
// 当上面 a 赋值给 b 的时候,a 还是存在的
// 如果你从其他语言转到 Rust,你可能会比较理解 copy 这个动作,但如果你有 c++ 基础,
// 你可能还会知道 c++ 有一个转移,以及左值右值的概念,左移右移
// Rust 也同样有转移的概念,就是我们下面要说的
// move 操作
// 在 move 的方式下,它的值的所有权,也就是 ownership 会发生改变,但是以上的基础数据类型,
// 默认执行的是 copy,而不执行 move,那什么样的类型会执行 move 呢
// struct、string以及大部分复杂的数据类型,他们都会执行move操作
// 为什么呢?因为复杂的结构将你的所有权交出去,会节省很大的空间,而且还会提高性能,以及防止你在
// 异步、并发时犯一些错误,这是非常有必要的
let string_item = String::from("aa");
// String 类型就把 ownership进行move操作, 这里就将 string_item 的所有权接管了
// 当所有权转移的那一刻,string_item 就不复存在了
let string_item_tt = string_item;
// 如果这时你试图打印,就会出现一个经典错误
println!("{string_item}"); // vorrow of moved value: 'string_item' value borrowed here after move
// 因为前面的数据类型都实现了 copy 的特质,默认的实现了 copy 操作
// 而 string 没有实现 copy 的特质,所以只能进行所有权的转移,这个就是 ownership
// 当然 ownership 不止如此,这里仅仅是一个概念引入
}
第五节 Rust 的内存管理模型
内存管理模型
第一类 C/C++
- 它们的内存管理模型会把内存的分配和释放下放给程序员,必须手动编写代码来实现内存的分配与释放
- 内存管理是纯粹手动的,写错了是你自己菜
- 对程序员的压力是最大的
- 代价就是性能是最好的(Rust在性能方面和这种类似,但是做了一定的取舍)
- 特别接近底层,最直接的操纵你的底层(再直接的就是汇编了),所以往往性能比较好
- New + delete | reference counting
第二类 Python、C#、Java 等等
- 现代编程语言的主流内存管理模型,将内存管理交给GC
- 交给GC了,龟派弟子突出一个猥琐
- 安全,但 stop the world 对性能的伤害巨大
- 程序员对GC爱憎分明,内存的分配和释放都相对比较安全一些,但是它有一个概念叫做 stop the world,这个东西对性能的伤害是非常非常巨大的,尤其是你对实时性要求非常高的时候,用这种内存管理模型,它们的性能是非常非常糟糕的,所以像游戏开发,就不适合用 Python、C#、Java 这类语言进行开发
- 可能有些人觉得 C# 在游戏领域非常受欢迎,但你仔细阅读一下 C# 你会发现,它做了专门一些改进,另外,很多游戏底层仍然是使用的 C++
- 程序员对GC爱憎分明,内存的分配和释放都相对比较安全一些,但是它有一个概念叫做 stop the world,这个东西对性能的伤害是非常非常巨大的,尤其是你对实时性要求非常高的时候,用这种内存管理模型,它们的性能是非常非常糟糕的,所以像游戏开发,就不适合用 Python、C#、Java 这类语言进行开发
第三类 Rust
- The Rust Compiler 最特殊的那一个
- Rust 的编译器是所有编程语言中最特殊的那一个,它会在编译器编译的这个时期来做一系列的检查,它在编译的时候,如果发现你的内存有一些问题的话,它直接不会让你通过。而且它通过所有权机制来限制你的这样的错误产生
- 好处显而易见,比如说并发时,通过一些规则来避免发生 data race(数据竞争),可以做到直接把错误扼杀在摇篮之中
- Rust 是如何做到这些的呢,整体的机制叫做 ownership (所有权机制),而 ownership 主要又分为以下几种
- Ownership rules & semantics(所有权规则&语义)
- Borrow Checker(借用检查) 在 Rust中,将「引用」称作 Borrow,所以引用在 Rust 中就叫「借用」
- Lifetime(借用的生命周期)你也可以叫引用的生命周期
- 还有一些其他规则像 RC、智能指针的一些限制
- 通过这些规则来管理内存,使 Rust 既达到了 C 和 C++ 的性能,又达到了 Python、C#、Java、Go 等语言的安全,需要着重强调 Rust 的安全性远远超出这些语言,因为它还有不变性、一些函数式的特性,所以它更安全,但是,Rust 的性能不一定比 C 和 C++ 更好,它只能是接近,因为很多底层(这里指的是计算机)的东西还是 C 和 C++, Rust 没办法和 C++ 比和 C 的交互,所以这些历史问题决定了 Rust 在性能上会比 C 和 C++ 稍微低一点。
- 理论基础上 Rust 和 C、C++ 的性能是一个水平上的,如果你非要比个高低的话,C > Rust > C++,因为 C++ 有虚表(如果C++不使用虚表,那它和 Rust、C 是完完全全一样的)
- 但是现实中,C > C++ > Rust,因为在现实使用中,Rust 还有一个和 C 交互的成本
- Rust 的编译器是所有编程语言中最特殊的那一个,它会在编译器编译的这个时期来做一系列的检查,它在编译的时候,如果发现你的内存有一些问题的话,它直接不会让你通过。而且它通过所有权机制来限制你的这样的错误产生
Stop the world
“Stop the world” 是与垃圾回收(Garbage Collection)相关的术语,它是指在进行垃圾回收时,系统暂停程序的运行。
这个属于主要用于描述一种全局性的暂停,即所有应用线程都被停止,以便垃圾回收器能够安全的进行工作。这种全局性的停止会导致一些潜在的问题,特别是对于需要低延迟和高性能的应用程序。
需要注意的是,并非所有的垃圾回收算法都需要”Stop the world”,有一些现代的垃圾回收器采用了一些技术来减小全局停顿的影响,比如并发垃圾回收和增量垃圾回收。
C/C++ 内存错误大全
内存泄漏(Memory Leaks)
int *ptr = new int; // 忘记释放内存 // delete ptr;
1
2
3
4
5
6
7
- 悬空/野指针(Dangling Pointers)
- ```C
int *ptr = new int;
delete ptr;
// ptr 现在是悬空指针
重复释放(Double Free)
int *ptr = new int; delete ptr; delete ptr;
1
2
3
4
5
6
- 数组越界(Array Out of Bounds)
- ```C
int arr[5];
arr[5] = 10;
使用已经释放的内存(Use After Free)
int *ptr = new int; delete ptr; *ptr = 10; // 使用已经释放的内存
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
- 堆栈溢出(Stack Overflow)
- 递归堆栈溢出
- 不匹配的 new/delete 或 malloc/free
#### Rust 的内存管理模型
- 所有权系统(Ownership System)
- 借用(Borrowing)
- 不可变引用(不可变借用)
- 可变引用(可变借用)
- 生命周期(Lifetimes)
- 引用计数(Reference Counting)
```Rust
fn main() {
// 所有权 copy & move
// copy
let c1 = 1;
let c2 = c1;
println!("{}", c1);
// c1 在赋值给 c2 的时候,c1 是还存在的,大多数编程语言不会直接给你释放掉
// 但是 rust 不一样,c1 如果是个基础类型,那么在 c1 赋值时会进行一个 copy 操作,所以它还在
// 但是有些数据类型,比方说字符串就不会执行 copy,而是执行 move,在 s1 给 s2 赋值时,
// 会将自己的所有权转移给 s2 ,当没有所有权时,就会被销毁
let s1 = String::from("value");
let s2 = s1;
println!("{s1}");
// 所有权根据类型不同,有不同的表现
// 如果我想用 s1 怎么办呢,执行一下 clone,相当于深拷贝
let s3 = s1.clone()
// 很多人对下面这个操作就非常不理解
// 这里可以用
get_length(s1);
// 这里又不可以用了
println!("{s1}");
// 想想为什么?
// 所有权 -> fn
// fn 结束后,s1 销毁了
// 性能非常棒
}
fn get_length(s: String) {
println!("String: {}", s);
}
fn get_length(s: String) -> usize {
println!("String: {}", s);
// 和 swift 一样,函数的最后一个语句可以不加 return,用这个当返回值返回给你
s.len()
// 其他语言可以将s传回去,但 rust 不会,当然其他语言也可以做到 rust 这个效果,比如
// c++ 的左值和右值,以及 c 的指针
}