🍉 加载中...


Comprehensive Rust 🦀 - 第一天

18 minute read

这是由 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)

标量类型

名称 类型 表示
有符号整数 i8i16i32i64i128isize -1001_000123i64
无符号整数 u8u16u32u64u128usize 012310u16
浮点数 f32f64 3.14-10.0e202f32
字符串 &str "foo"r#"\\"#
Unicode 标量值 char 'a''α''∞'
字节串 &[u8] b"abc"br#" " "#
布尔值 bool truefalse

位宽(字长)

  • iNuN, 和 fN N 位宽
  • isizeusize 指针的宽度
  • 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 是一个可变的字符串缓冲区。

函数

著名面试问题 FizzBu​​zz 的 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,如果函数在 mallocfree 之间提前返回,则会造成内存泄漏。即指针丢失,我们无法释放内存。

基于作用域的内存管理

构造函数和析构函数让您可以 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 获得自己的独立副本。
  • s1s2 超出范围时,它们各自释放自己的内存。

复制分配之前:

复制分配后:

简单来说,就是连同值一起拷贝了一份。老实说,一个很 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}
  • 分配后,p1p2 双方各自拥有自己的数据(看到这段话时,不知您是否有联想到上文提到的,现代 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}