Comprehensive Rust 🦀 - 第一天
这是由 Android 团队开发的为期四天的 Rust 课程。该课程涵盖了 Rust 的全部内容,从基本语法到高级主题,如泛型和错误处理。它还包括最后一天的 Android 特定内容。
示例
1fn main() { // 程序入口点
2 let mut x: i32 = 6; // 可变变量绑定
3 print!("{x}"); // 用于打印字符串到终端的宏
4 while x != 1 { // 表达式两边没有小括号
5 if x % 2 == 0 { // 数学运算和其他语言一样
6 x = x / 2;
7 } else {
8 x = 3 * x + 1;
9 }
10 print!(" -> {x}");
11 }
12 println!();
13}
特点
编译时内存安全
- 没有未初始化的变量
- 没有内存泄漏
- 没有双重释放
- 没有释放后使用
- 没有
NULL
指针 - 没有被遗忘的锁定互斥体
- 线程之间没有数据竞争
- 没有迭代器失效
运行时安全
- 数组访问有边界检查
- 定义了整数溢出行为
现代特色
- 枚举 / 模式匹配
- 泛型
- 零开销 FFI
- 零成本抽象
- 内置依赖管理器
- 严格的错误检测(编译器)
- 内置测试支持
- 优秀的语言服务器协议支持(LSP)
标量类型
名称 | 类型 | 表示 |
---|---|---|
有符号整数 | i8 , i16 , i32 , i64 , i128 , isize |
-10 , 0 , 1_000 , 123i64 |
无符号整数 | u8 , u16 , u32 , u64 , u128 , usize |
0 , 123 , 10u16 |
浮点数 | f32 , f64 |
3.14 , -10.0e20 , 2f32 |
字符串 | &str |
"foo" , r#"\\"# |
Unicode 标量值 | char |
'a' , 'α' , '∞' |
字节串 | &[u8] |
b"abc" , br#" " "# |
布尔值 | bool |
true , false |
位宽(字长)
iN
,uN
, 和fN
N 位宽isize
是usize
指针的宽度char
是 32 位宽bool
是 8 位宽
复合类型
名称 | 类型 | 表示 |
---|---|---|
数组 | [T; N] |
[20, 30, 40] , [0; 3] |
元组 | () , (T,) , (T1, T2) , … |
() , ('x',) , ('x', 1.2) , … |
数组赋值 / 访问
1fn main() {
2 let mut a: [i8; 10] = [42; 10];
3 a[5] = 0;
4 println!("a: {:?}", a);
5}
元组分配 / 访问
1fn main() {
2 let t: (i8, bool) = (7, true);
3 println!("1st index: {}", t.0);
4 println!("2nd index: {}", t.1);
5}
借用(引用)
与 C++ 一样,Rust 也有类似引用的东西,被称之为 “借用”:
1fn main() {
2 let mut x: i32 = 10;
3 let ref_x: &mut i32 = &mut x;
4 *ref_x = 20;
5 println!("x: {x}");
6}
Rust 中静态地禁止悬垂引用:
1fn main() {
2 let ref_x: &i32;
3 {
4 let x: i32 = 10;
5 ref_x = &x;
6 }
7 println!("ref_x: {ref_x}");
8}
注意事项
引用在 RUST 中被称为 “借用” 它引用的值。- Rust 会跟踪所有借用以确保它们的生命周期足够长。
- 在给 ref_x 赋值前必须取消所有对它的借用,类似于 C 和 C++ 指针。
- 在某些情况下,Rust 会自动取消借用,特别是在调用方法时 (try
ref_x.count_ones()
)。 - 声明为
mut
的借用可以在其生命周期内绑定到不同的值。
切片
下面示例中,切片 s 从 切片 a 中借用数据。
1fn main() {
2 let a: [i32; 6] = [10, 20, 30, 40, 50, 60];
3 println!("a: {a:?}");
4
5 let s: &[i32] = &a[2..4];
6 println!("s: {s:?}");
7}
String 与 str
Rust 中的两种字符串类型:
1fn main() {
2 let s1: &str = "World";
3 println!("s1: {s1}");
4
5 let mut s2: String = String::from("Hello ");
6 println!("s2: {s2}");
7 s2.push_str(s1);
8 println!("s2: {s2}");
9
10 let s3: &str = &s2[6..];
11 println!("s3: {s3}");
12}
术语:
&str
是对字符串切片的不可变引用。String
是一个可变的字符串缓冲区。
函数
著名面试问题 FizzBuzz 的 Rust 版本:
1fn main() {
2 fizzbuzz_to(20); // Defined below, no forward declaration needed
3}
4
5fn is_divisible_by(lhs: u32, rhs: u32) -> bool {
6 if rhs == 0 {
7 return false; // Corner case, early return
8 }
9 lhs % rhs == 0 // The last expression in a block is the return value
10}
11
12fn fizzbuzz(n: u32) -> () { // No return value means returning the unit type `()`
13 match (is_divisible_by(n, 3), is_divisible_by(n, 5)) {
14 (true, true) => println!("fizzbuzz"),
15 (true, false) => println!("fizz"),
16 (false, true) => println!("buzz"),
17 (false, false) => println!("{n}"),
18 }
19}
20
21fn fizzbuzz_to(n: u32) { // `-> ()` is normally omitted
22 for i in 1..=n {
23 fizzbuzz(i);
24 }
25}
方法
1struct Rectangle {
2 width: u32,
3 height: u32,
4}
5
6impl Rectangle {
7 fn area(&self) -> u32 {
8 self.width * self.height
9 }
10
11 fn inc_width(&mut self, delta: u32) {
12 self.width += delta;
13 }
14}
15
16fn main() {
17 let mut rect = Rectangle { width: 10, height: 5 };
18 println!("old area: {}", rect.area());
19 rect.inc_width(5);
20 println!("new area: {}", rect.area());
21}
重载
RUST 中没有重载,这是唯物主义折的胜利✌️。
变量
Rust 通过静态类型提供类型安全。默认情况下,变量绑定是不可变的:
1fn main() {
2 let x: i32 = 10;
3 println!("x: {x}");
4 // x = 20;
5 // println!("x: {x}");
6}
类型推断
Rust 通过检查变量如何被使用来推断类型:
1fn takes_u32(x: u32) {
2 println!("u32: {x}");
3}
4
5fn takes_i8(y: i8) {
6 println!("i8: {y}");
7}
8
9fn main() {
10 let x = 10;
11 let y = 20;
12
13 takes_u32(x);
14 takes_i8(y);
15 // takes_u32(y);
16}
静态变量 / 常量
声明常量
1const DIGEST_SIZE: usize = 3;
2const ZERO: Option<u8> = Some(42);
3
4fn compute_digest(text: &str) -> [u8; DIGEST_SIZE] {
5 let mut digest = [ZERO.unwrap_or(0); DIGEST_SIZE];
6 for (idx, &b) in text.as_bytes().iter().enumerate() {
7 digest[idx % DIGEST_SIZE] = digest[idx % DIGEST_SIZE].wrapping_add(b);
8 }
9 digest
10}
11
12fn main() {
13 let digest = compute_digest("Hello");
14 println!("Digest: {digest:?}");
15}
根据 Rust RFC Book,这些常量在使用时内联。
声明静态变量
1static BANNER: &str = "Welcome to RustOS 3.14";
2
3fn main() {
4 println!("{BANNER}");
5}
如 Rust RFC Book 中所述,这些变量在使用时并未内联,并且具有实际关联的内存位置。这对于不安全和嵌入式代码很有用,并且静态变量的生命周期将贯穿整个程序执行过程。
阴影(Shadowing)
您可以通过阴影特性隐藏变量,包括来自外部作用域的变量和来自同一作用域的变量:
1fn main() {
2 let a = 10;
3 println!("before: {a}");
4
5 {
6 let a = "hello";
7 println!("inner scope: {a}");
8
9 let a = true;
10 println!("shadowed in inner scope: {a}");
11 }
12
13 println!("after: {a}");
14}
只看表象的话… 就是在特定作用域内的变量覆盖对吧?
内存管理
传统上,语言分为两大类:
- 通过手动内存管理实现完全控制:C、C++、Pascal……
- 通过运行时的自动内存管理实现完全安全:Java、Python、Go、Haskell ……
Rust 提供了一种新的组合:
通过明确的所有权概念,在编译时执行正确的内存管理实现完全控制和内存安全。
基础知识
堆 / 栈
-
栈(Stack):局部变量的连续内存区域。
- 值具有在编译时已知的固定大小。
- 只需移动一个堆栈指针,因此操作极快。
- 易于管理:遵循函数调用。
- 很棒的内存位置。
-
堆(Heap):存储函数调用之外的值。
- 值具有在运行时确定的动态大小。
- 比栈稍慢:需要一些簿记。
- 不保证内存局部性。
TODO:这部分内容显然存在问题,有待完善。
栈内存
创建一个 String
将固定大小的数据放在堆栈上,将动态大小的数据放在堆上:
1fn main() {
2 let s1 = String::from("Hello");
3}
手动内存管理
您自己分配和释放堆内存。如果不小心,这可能会导致崩溃、错误、安全漏洞和内存泄漏。
以下是一个 C 语言中手动管理内存的示例:
1void foo(size_t n) {
2 int* int_array = (int*)malloc(n * sizeof(int));
3 //
4 // ... lots of code
5 //
6 free(int_array);
7}
您必须为您调用 malloc
分配的每个指针调用 free
,如果函数在 malloc
和 free
之间提前返回,则会造成内存泄漏。即指针丢失,我们无法释放内存。
基于作用域的内存管理
构造函数和析构函数让您可以 Hook 对象的生命周期。
通过将指针包装在对象中,您可以在对象被销毁时释放内存。编译器保证会发生这种情况,即使引发异常也是如此。
这通常称为 resource acquisition is initialization(RAII),并为您提供智能指针。
以下是一个 C++ 示例:
1void say_hello(std::unique_ptr<Person> person) {
2 std::cout << "Hello " << person->name << std::endl;
3}
std::unique_ptr
对象被分配在栈上,并指向分配在堆上的内存。- 在
say_hello
结束时,std::unique_ptr
析构函数将运行。 - 析构函数释放它指向的
Person
对象。
将所有权传递给函数时使用特殊的移动构造函数:
1std::unique_ptr<Person> person = find_person("Carla");
2say_hello(std::move(person));
自动内存管理(GC)
手动和基于作用域的内存管理的替代方法是自动内存管理:
- 从不需要显式分配或释放内存。
- 垃圾收集器自动查找并释放未使用的内存。
RUST 的内存管理
Rust 的内存管理是一个混合体:
- 像 Java 一样安全和正确,但没有垃圾收集器。
- 根据您选择的抽象(或抽象组合),可以是单个唯一指针、引用计数或原子引用计数。
- 像 C++ 一样基于作用域,但编译器强制完全遵守。
- Rust 用户可以根据情况选择正确的抽象,有些甚至在运行时没有成本,比如 C。
它通过明确地所有权机制用以实现生命周期管理,来做到上述提到的优点。关于缺点,则是一些前期的复杂性,并且编译器可能会拒绝有效代码。
所有权
所有变量绑定都有一个有效作用域,在其作用域之外使用变量是错误的:
1struct Point(i32, i32);
2
3fn main() {
4 {
5 let p = Point(3, 4);
6 println!("x: {}", p.0);
7 }
8 println!("y: {}", p.1);
9}
- 在作用域的末尾,变量被_丢弃_,数据被释放。
- 析构函数可以在这里运行以释放资源。
- 我们说变量_拥有_值。
转移方式
赋值
1fn main() {
2 let s1: String = String::from("Hello!");
3 let s2: String = s1;
4 println!("s2: {s2}");
5 // println!("s1: {s1}");
6}
s2
的分配转移s1
的所有权。- 数据已移出,无法再访问
s1
。 - 当
s1
超出范围时,什么也不会发生(它没有所有权)。 - 当
s2
超出范围时,字符串数据将被释放。 - 总是只有一个变量绑定拥有一个值。
传参
当您将值传递给函数时,该值将分配给函数参数。这会转移所有权:
1fn say_hello(name: String) {
2 println!("Hello {name}")
3}
4
5fn main() {
6 let name = String::from("Alice");
7 say_hello(name);
8 // say_hello(name);
9}
简单示例
1fn main() {
2 let s1: String = String::from("Rust");
3 let s2: String = s1;
4}
- 来自的堆数据
s1
被重新用于s2
。 - 当
s1
超出范围时,什么也不会发生(它没有所有权)。
移动到 s2
之前:
移动到 s2
之后:
从上图可以看出,移动到 s2
之后,s1
便不能再被访问了(inaccessible)。
双重释放
对于双重释放问题,现代 C++ 以不同的方式解决了这个问题:
1std::string s1 = "Cpp";
2std::string s2 = s1; // Duplicate the data in s1.
- 来自的堆数据
s1
被复制并s2
获得自己的独立副本。 - 当
s1
和s2
超出范围时,它们各自释放自己的内存。
复制分配之前:
复制分配后:
简单来说,就是连同值一起拷贝了一份。老实说,一个很 Low 的解决方法。
复制 / 克隆
虽然移动语义是默认的,但默认情况下会复制某些类型:
1fn main() {
2 let x = 42;
3 let y = x;
4 println!("x: {x}");
5 println!("y: {y}");
6}
因为这些类型实现了 Copy
特性。
您也可以为自定义的类型加入复制语义:
1#[derive(Copy, Clone, Debug)]
2struct Point(i32, i32);
3
4fn main() {
5 let p1 = Point(3, 4);
6 let p2 = p1;
7 println!("p1: {p1:?}");
8 println!("p2: {p2:?}");
9}
- 分配后,
p1
都p2
双方各自拥有自己的数据(看到这段话时,不知您是否有联想到上文提到的,现代 C++ 中解决双重释放所采用的方式呢?)。 - 可以使用
p1.clone()
显式复制数据。
Rust 中的标量类型,默认都实现了
Copy
特性。
借用
- 不可变借用:同一时间可被多个变量借用。
- 可变借用:同一时间只能被一个变量借用。
传参
可以让函数借用值,而不是在调用函数时转移所有权:
1#[derive(Debug)]
2struct Point(i32, i32);
3
4fn add(p1: &Point, p2: &Point) -> Point {
5 Point(p1.0 + p2.0, p1.1 + p2.1)
6}
7
8fn main() {
9 let p1 = Point(3, 4);
10 let p2 = Point(10, 20);
11 let p3 = add(&p1, &p2);
12 println!("{p1:?} + {p2:?} = {p3:?}");
13}
- 该
add
函数_借用_两个 Point 并返回一个新的 Point。 - 调用者保留了调用函数时所传出参数的所有权。
赋值
在赋值时可以让其借用,而不是转移所有权。
1fn main() {
2 let mut a: i32 = 10;
3 let b: &i32 = &a;
4
5 {
6 let c: &mut i32 = &mut a;
7 *c = 20;
8 }
9
10 println!("a: {a}");
11 println!("b: {b}");
12}
生命周期
生命周期总是由编译器推断,即你不能自己分配生命周期。但是,开发者可以通过添加生命周期注释创建约束,编译器会验证是否存在有效的解决方案。
- 生命周期可以省略:
add(p1: &Point, p2: &Point) -> Point
。 - 生命周期可以是显式的:
&'a Point
,&'document str
。 - 将
&'a Point
理解为 “一个至少在生命周期 a 内有效的借用 Point”。
通常只有借用需要显式的添加生命周期注释,目的则是对多个借用的生命周期进行统一。
函数调用
除了借用其参数外,函数还可以返回借用的值:
1#[derive(Debug)]
2struct Point(i32, i32);
3
4fn left_most<'a>(p1: &'a Point, p2: &'a Point) -> &'a Point {
5 if p1.0 < p2.0 { p1 } else { p2 }
6}
7
8fn main() {
9 let p1: Point = Point(10, 10);
10 let p2: Point = Point(20, 20);
11 let p3: &Point = left_most(&p1, &p2);
12 println!("left-most point: {:?}", p3);
13}
'a
是通用参数,由编译器推断。- 生命周期以
'
开头,并且'a
是一个典型的默认名称。 - 将 &‘a Point 理解为 “一个至少在生命周期 a 内有效的借用 Point”。
- 当参数在不同的范围内时,least 部分很重要。
数据结构
如果数据类型需要存储借用的数据,则必须使用生命周期对其进行注释:
1#[derive(Debug)]
2struct Highlight<'doc>(&'doc str);
3
4fn erase(text: String) {
5 println!("Bye {text}!");
6}
7
8fn main() {
9 let text = String::from("The quick brown fox jumps over the lazy dog.");
10 let fox = Highlight(&text[4..19]);
11 let dog = Highlight(&text[35..43]);
12 // erase(text);
13 println!("{fox:?}");
14 println!("{dog:?}");
15}