🍉 加载中...


Comprehensive Rust 🦀 - 第二天

23 minute read

这是由 Android 团队开发的为期四天的 Rust 课程。该课程涵盖了 Rust 的全部内容,从基本语法到高级主题,如泛型和错误处理。它还包括最后一天的 Android 特定内容。

现在我们已经了解了相当多的 Rust,我们将继续:

  • 结构、枚举、方法。
  • 模式匹配:解构枚举、结构和数组。
  • 控制流构造:ifif letwhilewhile letbreak 和 continue
  • 标准库:StringOptionResultVecHashMapRcArc
  • 模块:可见性、路径和文件系统层次结构。

结构体(Struct)

与 C 和 C++ 一样,Rust 支持自定义结构:

 1struct Person {
 2    name: String,
 3    age: u8,
 4}
 5
 6fn main() {
 7    let mut peter = Person {
 8        name: String::from("Peter"),
 9        age: 27,
10    };
11    println!("{} is {} years old", peter.name, peter.age);
12    
13    peter.age = 28;
14    println!("{} is {} years old", peter.name, peter.age);
15    
16    let jackie = Person {
17        name: String::from("Jackie"),
18        ..peter
19    };
20    println!("{} is {} years old", jackie.name, jackie.age);
21}

.. 语法允许我们从旧结构中复制大部分字段,而无需显式地将其全部键入。它必须始终是最后一个元素。

元组结构体(Tuple Struct)

如果字段名称不重要,您可以使用元组结构体:

1struct Point(i32, i32);
2
3fn main() {
4    let p = Point(17, 23);
5    println!("({}, {})", p.0, p.1);
6}

这通常用于单字段包装器(称为新类型):

 1struct PoundOfForce(f64);
 2struct Newtons(f64);
 3
 4fn compute_thruster_force() -> PoundOfForce {
 5    todo!("Ask a rocket scientist at NASA")
 6}
 7
 8fn set_thruster_force(force: Newtons) {
 9    // ...
10}
11
12fn main() {
13    let force = compute_thruster_force();
14    set_thruster_force(force);
15}

字段速记语法

如果您已经有了具有正确名称的变量,那么您可以使用简写形式创建结构:

 1#[derive(Debug)]
 2struct Person {
 3    name: String,
 4    age: u8,
 5}
 6
 7impl Person {
 8    fn new(name: String, age: u8) -> Person {
 9        Person { name, age }
10    }
11}
12
13fn main() {
14    let peter = Person::new(String::from("Peter"), 27);
15    println!("{peter:?}");
16}

枚举(Enum)

关键字 enum 允许创建具有几个不同变体(Variant)的类型:

 1fn generate_random_number() -> i32 {
 2    4  // Chosen by fair dice roll. Guaranteed to be random.
 3}
 4
 5#[derive(Debug)]
 6enum CoinFlip {
 7    Heads,
 8    Tails,
 9}
10
11fn flip_coin() -> CoinFlip {
12    let random_number = generate_random_number();
13    if random_number % 2 == 0 {
14        return CoinFlip::Heads;
15    } else {
16        return CoinFlip::Tails;
17    }
18}
19
20fn main() {
21    println!("You got: {:?}", flip_coin());
22}

变体有效载荷(Variant Payloads)

您可以定义更丰富的枚举,其中变体携带数据。然后,您可以使用该 match 语句从每个变体中提取数据:

 1enum WebEvent {
 2    PageLoad,                 // 没有有效载荷的变体
 3    KeyPress(char),           // 元组结构体变体
 4    Click { x: i64, y: i64 }, // 完整结构体变体
 5}
 6
 7#[rustfmt::skip]
 8fn inspect(event: WebEvent) {
 9    match event {
10        WebEvent::PageLoad       => println!("page loaded"),
11        WebEvent::KeyPress(c)    => println!("pressed '{c}'"),
12        WebEvent::Click { x, y } => println!("clicked at x={x}, y={y}"),
13    }
14}
15
16fn main() {
17    let load = WebEvent::PageLoad;
18    let press = WebEvent::KeyPress('x');
19    let click = WebEvent::Click { x: 20, y: 80 };
20
21    inspect(load);
22    inspect(press);
23    inspect(click);
24}

枚举大小(Enum Size)

Rust 枚举是紧密打包的,考虑了对齐引起的约束:

 1use std::mem::{align_of, size_of};
 2
 3macro_rules! dbg_size {
 4    ($t:ty) => {
 5        println!("{}: size {} bytes, align: {} bytes",
 6                 stringify!($t), size_of::<$t>(), align_of::<$t>());
 7    };
 8}
 9
10enum Foo {
11    A,
12    B,
13}
14
15fn main() {
16    dbg_size!(Foo);
17}

方法(Method)

Rust 允许您将函数与新类型相关联。用一个 impl 块来做到这一点:

 1#[derive(Debug)]
 2struct Person {
 3    name: String,
 4    age: u8,
 5}
 6
 7impl Person {
 8    fn say_hello(&self) {
 9        println!("Hello, my name is {}", self.name);
10    }
11}
12
13fn main() {
14    let peter = Person {
15        name: String::from("Peter"),
16        age: 27,
17    };
18    peter.say_hello();
19}

接收者

&self 表明方法不可变地借用了对象。方法还有其他可能的接收者:

  • &self:使用共享且不可变的引用从调用者那里借用对象。 之后可以再次使用该对象。
  • &mut self:使用唯一且可变的引用从调用者那里借用对象。 之后可以再次使用该对象。
  • self:获取对象的所有权并将其从调用者那里移开。 该方法成为对象的所有者。 当方法返回时,该对象将被删除(释放),除非它的所有权被显式传输。
  • mut self:与上面相同,但是当方法拥有对象时,它也可以改变它。 完全所有权并不自动意味着可变性。
  • 没有接收者:这成为结构上的静态方法。 通常用于创建按照约定称为 new 的构造函数。

除了 self 的变体之外,还有一些 特殊的包装器类型 可以作为接收者类型,例如Box<Self>

示例

 1#[derive(Debug)]
 2struct Race {
 3    name: String,
 4    laps: Vec<i32>,
 5}
 6
 7impl Race {
 8    fn new(name: &str) -> Race {  // No receiver, a static method
 9        Race { name: String::from(name), laps: Vec::new() }
10    }
11
12    fn add_lap(&mut self, lap: i32) {  // Exclusive borrowed read-write access to self
13        self.laps.push(lap);
14    }
15
16    fn print_laps(&self) {  // Shared and read-only borrowed access to self
17        println!("Recorded {} laps for {}:", self.laps.len(), self.name);
18        for (idx, lap) in self.laps.iter().enumerate() {
19            println!("Lap {idx}: {lap} sec");
20        }
21    }
22
23    fn finish(self) {  // Exclusive ownership of self
24        let total = self.laps.iter().sum::<i32>();
25        println!("Race {} is finished, total lap time: {}", self.name, total);
26    }
27}
28
29fn main() {
30    let mut race = Race::new("Monaco Grand Prix");
31    race.add_lap(70);
32    race.add_lap(68);
33    race.print_laps();
34    race.add_lap(71);
35    race.print_laps();
36    race.finish();
37    // race.add_lap(42);
38}

Rust 在调用方法时支持自动引用和解引用,即Rust 自动添加 &* ,muts 以便该对象与方法签名匹配。

控制流

正如我们所见,if 是 Rust 中的一个表达式。它用于有条件地评估两个 块(Block) 之一,但块可以有一个值,然后成为 if 表达式的值。其他控制流表达式在 Rust 中的工作方式类似。

Blocks

Rust 中的块具有值和类型,值是块的最后一个表达式:

 1fn main() {
 2    let x = {
 3        let y = 10;
 4        println!("y: {y}");
 5        let z = {
 6            let w = {
 7                3 + 4
 8            };
 9            println!("w: {w}");
10            y * w
11        };
12        println!("z: {z}");
13        z - y
14    };
15    println!("x: {x}");
16}

同样的规则也适用于函数,函数体的值就是返回值:

1fn double(x: i32) -> i32 {
2    x + x
3}
4
5fn main() {
6    println!("doubled: {}", double(7));
7}

如果最后一个表达式以 ; 结尾,则结果值与类型为 ()

if 表达式

1fn main() {
2    let mut x = 10;
3    if x % 2 == 0 {
4        x = x / 2;
5    } else {
6        x = 3 * x + 1;
7    }
8}

可以将其用作表达式:

1fn main() {
2    let mut x = 10;
3    x = if x % 2 == 0 {
4        x / 2
5    } else {
6        3 * x + 1
7    };
8}

务必牢记 语句表达式 的区别是什么。

if let 表达式

如果要将值与模式匹配,可以使用 if let

1fn main() {
2    let arg = std::env::args().next();
3    if let Some(value) = arg {
4        println!("Program name: {value}");
5    } else {
6        println!("Missing name?");
7    }
8}

有关Rust 模式的更多详细信息,请参阅模式匹配。

while 表达式

 1fn main() {
 2    let mut x = 10;
 3    while x != 1 {
 4        x = if x % 2 == 0 {
 5            x / 2
 6        } else {
 7            3 * x + 1
 8        };
 9    }
10    println!("Final x: {x}");
11}

while let 表达式

1fn main() {
2    let v = vec![10, 20, 30];
3    let mut iter = v.into_iter();
4
5    while let Some(x) = iter.next() {
6        println!("x: {x}");
7    }
8}

这里涉及到了迭代器相关知识点,但这并非 Rust 特有,因此不做详述。

for 表达式

 1fn main() {
 2    let v = vec![10, 20, 30];
 3
 4    for x in v {
 5        println!("x: {x}");
 6    }
 7    
 8    for i in (0..10).step_by(2) {
 9        println!("i: {i}");
10    }
11}

(0..10) 是实现 Iterator 特性的范围(Range)。

loop 表达式

 1fn main() {
 2    let mut x = 10;
 3    loop {
 4        x = if x % 2 == 0 {
 5            x / 2
 6        } else {
 7            3 * x + 1
 8        };
 9        if x == 1 {
10            break;
11        }
12    }
13    println!("Final x: {x}");
14}

用一个值(例如 break 8)打破循环并打印出来。

match 表达式

关键字 match 用于将值与一个或多个模式匹配。从这个意义上说,它就像一系列if let表达式:

 1fn main() {
 2    match std::env::args().next().as_deref() {
 3        Some("cat") => println!("Will do cat things"),
 4        Some("ls")  => println!("Will ls some files"),
 5        Some("mv")  => println!("Let's move some files"),
 6        Some("rm")  => println!("Uh, dangerous!"),
 7        None        => println!("Hmm, no program name?"),
 8        _           => println!("Unknown program name!"),
 9    }
10}

模式匹配(Pattern Matching)

match 关键字使您可以将一个值与一个或多个模式相匹配,匹配是从上到下进行的。

 1fn main() {
 2    let input = 'x';
 3
 4    match input {
 5        'q'                   => println!("Quitting"),
 6        'a' | 's' | 'w' | 'd' => println!("Moving around"),
 7        '0'..='9'             => println!("Number input"),
 8        _                     => println!("Something else"),
 9    }
10}

_ 模式是匹配任何值的通配符模式。.. 可以根据需要扩展,1..=5 代表一个包含范围。

解构枚举

模式也可用于将变量绑定到您的部分值,这就是检查类型结构的方式。让我们用一个简单的 enum 类型进行演示:

 1enum Result {
 2    Ok(i32),
 3    Err(String),
 4}
 5
 6fn divide_in_two(n: i32) -> Result {
 7    if n % 2 == 0 {
 8        Result::Ok(n / 2)
 9    } else {
10        Result::Err(format!("cannot divide {n} into two equal parts"))
11    }
12}
13
14fn main() {
15    let n = 100;
16    match divide_in_two(n) {
17        Result::Ok(half) => println!("{n} divided in two is {half}"),
18        Result::Err(msg) => println!("sorry, an error happened: {msg}"),
19    }
20}

解构结构体

 1struct Foo {
 2    x: (u32, u32),
 3    y: u32,
 4}
 5
 6#[rustfmt::skip]
 7fn main() {
 8    let foo = Foo { x: (1, 2), y: 3 };
 9    match foo {
10        Foo { x: (1, b), y } => println!("x.0 = 1, b = {b}, y = {y}"),
11        Foo { y: 2, x: i }   => println!("y = 2, x = {i:?}"),
12        Foo { y, .. }        => println!("y = {y}, other fields were ignored"),
13    }
14}

解构数组

您可以通过匹配元素来解构数组、元组和切片:

 1#[rustfmt::skip]
 2fn main() {
 3    let triple = [0, -2, 3];
 4    println!("Tell me about {triple:?}");
 5    match triple {
 6        [0, y, z] => println!("First is 0, y = {y}, and z = {z}"),
 7        [1, ..]   => println!("First is 1 and the rest were ignored"),
 8        _         => println!("All elements were ignored"),
 9    }
10}

指出 .. 将如何扩展以考虑不同数量的元素。使用模式 [.., b][a@..,b] 显式的与尾部进行匹配。

匹配守卫(Match Guards)

匹配的时候,可以给一个模式加上 guard。这是一个任意的布尔表达式,仅在模式匹配后才会被执行:

 1#[rustfmt::skip]
 2fn main() {
 3    let pair = (2, -2);
 4    println!("Tell me about {pair:?}");
 5    match pair {
 6        (x, y) if x == y     => println!("These are twins"),
 7        (x, y) if x + y == 0 => println!("Antimatter, kaboom!"),
 8        (x, _) if x % 2 == 1 => println!("The first one is odd"),
 9        _                    => println!("No correlation..."),
10    }
11}

标准库

Rust 带有一个标准库,它有助于建立一组由 Rust 库和程序使用的通用类型。这样,两个库就可以顺利地协同工作,因为它们都使用相同的 String 类型。

常见的词汇类型包括:

  • OptionResult类型:用于可选值和错误处理
  • String:用于自有数据的默认字符串类型。
  • Vec: 一个标准的可扩展向量。
  • HashMap:具有可配置哈希算法的哈希映射类型。
  • Box:堆分配数据的拥有指针。
  • Rc:堆分配数据的共享引用计数指针。

事实上,Rust 包含标准库的几个层次:coreallocstd

  • core 包括最基本的类型和函数,它们不依赖于 libc、分配器甚至操作系统的存在。
  • alloc 包括需要全局堆分配器的类型,例如 VecBoxArc
  • 嵌入式 Rust 应用程序通常只使用 core,有时使用 alloc

Option & Result

Option 与 Result 代表可选数据:

1fn main() {
2    let numbers = vec![10, 20, 30];
3    let first: Option<&i8> = numbers.first();
4    println!("first: {first:?}");
5
6    let idx: Result<usize, usize> = numbers.binary_search(&10);
7    println!("idx: {idx:?}");
8}

String

String 是标准的堆分配可增长 UTF-8 字符串缓冲区:

 1fn main() {
 2    let mut s1 = String::new();
 3    s1.push_str("Hello");
 4    println!("s1: len = {}, capacity = {}", s1.len(), s1.capacity());
 5
 6    let mut s2 = String::with_capacity(s1.len() + 1);
 7    s2.push_str(&s1);
 8    s2.push('!');
 9    println!("s2: len = {}, capacity = {}", s2.len(), s2.capacity());
10
11    let s3 = String::from("🇨🇭");
12    println!("s3: len = {}, number of chars = {}", s3.len(),
13             s3.chars().count());
14}

String 实现了 Deref<Target = str>,这意味着您可以直接在 String 调用 str 方法。

当人们提到字符串时,他们可能在谈论 &strString

Vec

Vec 是标准的可调整大小的堆分配缓冲区:

 1fn main() {
 2    let mut v1 = Vec::new();
 3    v1.push(42);
 4    println!("v1: len = {}, capacity = {}", v1.len(), v1.capacity());
 5
 6    let mut v2 = Vec::with_capacity(v1.len() + 1);
 7    v2.extend(v1.iter());
 8    v2.push(9999);
 9    println!("v2: len = {}, capacity = {}", v2.len(), v2.capacity());
10
11    // Canonical macro to initialize a vector with elements.
12    let mut v3 = vec![0, 0, 1, 2, 3, 4];
13
14    // Retain only the even elements.
15    v3.retain(|x| x % 2 == 0);
16    println!("{v3:?}");
17
18    // Remove consecutive duplicates.
19    v3.dedup();
20    println!("{v3:?}");
21}

Vec 实现了 Deref<Target = [T]>,这意味着您可以直接在 Vec 上调用 Slice 方法。

vec![...] 是一个可以代替 Vec::new() 使用的规范宏,它支持向向量添加初始元素。

HashMap

具有针对 HashDoS 攻击保护的标准哈希映射:

 1use std::collections::HashMap;
 2
 3fn main() {
 4    let mut page_counts = HashMap::new();
 5    page_counts.insert("Adventures of Huckleberry Finn".to_string(), 207);
 6    page_counts.insert("Grimms' Fairy Tales".to_string(), 751);
 7    page_counts.insert("Pride and Prejudice".to_string(), 303);
 8
 9    if !page_counts.contains_key("Les Misérables") {
10        println!("We know about {} books, but not Les Misérables.",
11                 page_counts.len());
12    }
13
14    for book in ["Pride and Prejudice", "Alice's Adventure in Wonderland"] {
15        match page_counts.get(book) {
16            Some(count) => println!("{book}: {count} pages"),
17            None => println!("{book} is unknown.")
18        }
19    }
20
21    // Use the .entry() method to insert a value if nothing is found.
22    for book in ["Pride and Prejudice", "Alice's Adventure in Wonderland"] {
23        let page_count: &mut i32 = page_counts.entry(book.to_string()).or_insert(0);
24        *page_count += 1;
25    }
26
27    println!("{page_counts:#?}");
28}

Box

Box 是指向堆上数据的拥有指针:

1fn main() {
2    let five = Box::new(5);
3    println!("five: {}", *five);
4}

Box<T> 实现了 Deref<Target = T> ,这意味着您可以直接在 Box<T> 上调用 T 的方法。

Box 类似于 C++ 中的 std::unique_ptr,只是它提供了不为空的保证。

递归数据结构

递归数据类型或具有动态大小的数据类型需要使用 Box

 1#[derive(Debug)]
 2enum List<T> {
 3    Cons(T, Box<List<T>>),
 4    Nil,
 5}
 6
 7fn main() {
 8    let list: List<i32> = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
 9    println!("{list:?}");
10}

如果这里没有使用 Box 并且我们试图将一个 List 直接嵌入到 List 中,编译器将不会计算内存中结构的固定大小,它看起来是无限的。

Box 解决了这个问题,因为它与常规指针具有相同的大小,并且仅指向堆中 List 的下一个元素。

删除 List 定义中的 Box 并显示编译器错误。 “间接递归”暗示您可能想要使用 Box 或某种引用,而不是直接存储值。

利基优化(Niche Optimization)

 1#[derive(Debug)]
 2enum List<T> {
 3    Cons(T, Box<List<T>>),
 4    Nil,
 5}
 6
 7fn main() {
 8    let list: List<i32> = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
 9    println!("{list:?}");
10}

Box 不能为空,因此指针始终有效且非空。 这允许编译器优化内存布局:

RC

Rc 是引用计数的共享指针,其确保只要有引用,它包含的值就有效,就像 C++ 的 std::shared_ptr

当您需要从多个地方引用相同的数据时使用它:

1use std::rc::Rc;
2
3fn main() {
4    let mut a = Rc::new(10);
5    let mut b = a.clone();
6
7    println!("a: {a}");
8    println!("b: {b}");
9}

如果需要改变 Rc 中的数据,则需要将数据包装在 Cell 或 RefCell 等类型中。如果您处于多线程上下文中,请参阅 Arc。您可以将共享指针降级为 弱指针(Weak pointer),以创建将被丢弃的循环。

您可以调用 downgrade()RC 降级为弱引用计数对象,以创建将被正确删除的循环(可能与 RefCell 结合使用)。

模块

crates 是一棵模块树,其中二进制(Binary) crate 创建可执行文件,而库(Lib) crate 编译为库。

模块定义组织、范围,并且是本节的重点。

 1mod foo {
 2    pub fn do_something() {
 3        println!("In the foo module");
 4    }
 5}
 6
 7mod bar {
 8    pub fn do_something() {
 9        println!("In the bar module");
10    }
11}
12
13fn main() {
14    foo::do_something();
15    bar::do_something();
16}

可见性

默认情况下,模块项是私有的(隐藏实现细节)。

父项和同级项始终可见。换句话说,如果一个项目在 module 中可见foo,那么它在 foo 的所有后代中都是可见的。

 1mod outer {
 2    fn private() {
 3        println!("outer::private");
 4    }
 5
 6    pub fn public() {
 7        println!("outer::public");
 8    }
 9
10    mod inner {
11        fn private() {
12            println!("outer::inner::private");
13        }
14
15        pub fn public() {
16            println!("outer::inner::public");
17            super::private();
18        }
19    }
20}
21
22fn main() {
23    outer::public();
24}

使用 pub 关键字使模块公开。此外,还有高级 pub(...) 说明符来限制可见性的范围。不太常见的是,您可以提供特定路径的可见性。

路径

作为相对路径解析:

  • fooself::foo 引用当前模块中的 foo
  • super::foo 引用父模块中的 foo

作为绝对路径解析:

  • crate::foo 引用当前 crate 根目录中的 foo
  • bar::foo 指的是 bar crate 中的 foo

文件系统层次结构

模块内容可以省略:

1mod garden;

模块 garden 内容位于:

  • src/garden.rs(现代 Rust 2018 风格)
  • src/garden/mod.rs(较旧的 Rust 2015 风格)

garden::vegetables同样,可以在以下位置找到一个模块:

  • src/garden/vegetables.rs(现代 Rust 2018 风格)
  • src/garden/vegetables/mod.rs(较旧的 Rust 2015 风格)

crate 在:

  • src/lib.rs(对于库 crate)
  • src/main.rs(对于二进制 crate)